1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-11 08:22:53 +00:00

CryptoExchange V12 (#281)

* Result types:
  * (Web)CallResult types are replaced by HttpResult, WebSocketResult and QueryResult with the same logic
  * Updated result types to record type
  * Result creation can be done with (Http/WebSocket/Query)Result.Ok(..) and .Fail(..)
  * Removed implicit result type conversion to bool, `if (result)` no longer works, instead use `if (result.Success)`
  * Replaced CallResult.SuccessResult with CallResult.Ok()
  * Fixed result object nullability hinting, for example Data might be null if Success isn't checked for true

* Parameters & serialization:
  * Added support for `enabled` and `disabled` strings to bool converter
  * Removed ParameterCollection type, has been replaced by Parameters type
  * Removed ArraySerialization, OrderParameters and ParameterOrderComparer properties from RestApiClient, moved to ParameterSerializationsSettings
  * Updated RestRequestConfiguration in AuthenticationProvider.ProcessRequest to contain the full RequestDefinition instead of copied fields	

* Clients:
  * Updated Api client constructor logging parameter from ILogger to ILoggerFactory? 
  * Added Api client constructor exchange name parameter
  * Added ToString overrides on base API types
  * Added Exchange property on BaseApiClient
  * Added ApiCredentials property on IRestApiClient and ISocketApiClient interfaces
  * Updated ILogger source from client name to topic specific client name
  * Removed logging from client creation
  * Fixed BaseRestClient SetApiCredentials not marked as virtual

* Rest:
  * Added BaseAddress to RequestDefinition object
  * Updated RestApiClient AuthenticationProvider logic from private to protected and virtual
  * Removed RestApiClient.SendAsync baseAddress parameter removed
  * Removed RestApiClient.SendAsync without type parameter

* WebSocket:
  * Updated MessageRouting definition into CreateForEvent for subscriptions and CreateForQuery for queries
  * Improved Query type safety with CeateForQuery which allows second parameter for specifying the result type
  * Renamed MessageRouter.CreateWithoutHandler to CreateVoid
  * Updated SocketApiClient.GetSocketConnection to check connection uri instead of Tag for finding compatible connections
  * Removed unused UnhandledMessageExpected property SocketApiClient
  * Fixed issue in SocketApiClient.GetSocketConnection causing requests to always wait the full max 10 seconds when there was a reconnecting socket
	
* Shared APIs:
  * Updated Option definitions to always require the exchange name as first parameter
  * Added missing dedicated option types
  * Added Discover method on ISharedClient interface, returning info on supported capabilities and operations
  * Added SharedRequest GetParamValue helper method accepting multiple parameter names
  * Added ResetStaticExchangeParameters method on ExchangeParameters
  * Added Status property to SharedWithdrawal model
  * Added TradingModes property to SharedBalance model
  * Updated ExchangeSymbolCache to support multiple environments and additional key separation
  * Updated Shared ExchangeParameters parameter names to be case insensitive
  * Updated code comments
  * Replaced ExchangeResult with ExchangeCallResult type
  * Removed AsExchangeResult/ExchangeWebResult
  * Removed TradingMode from the response model, only maintained on models where it makes sense
  * Removed IListenKey support, listen keys now rely on internal management with TokenManager

* Rate limiting:
  * Fixed websocket connection attempts counting towards rate limit even when server could not be reached
  * Removed host from rate limit methods, now part of the already provided RequestDefinition
  * Added amount parameter to RateLimit Reset method to allow partially resetting the limit

* Added TokenManager implementation for automatic listenkey/token management
* Added UserClientProvider base class
* Added async streaming on UserDataTracker items with StreamUpdatesAsync
* Added cancellation token support to UserDataTracker starting
* Added Unit type for non-result types
* Added ServerError constructor taking ErrorType and message to make it easier to create
* Added SupportedEnvironments property to PlatformInfo
* Updated SymbolOrderBook DoResyncAsync to return CallResult instead of CallResult<bool> which was redundant
* Various small performance improvements
This commit is contained in:
Jan Korf
2026-06-29 10:38:09 +02:00
committed by GitHub
parent afb84a1bf0
commit e823114623
285 changed files with 7029 additions and 3658 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
## Result pattern
Same `WebCallResult<T>` / `CallResult<T>` everywhere. Always check `.Success`. `.Exchange` property identifies which exchange responded — useful for logging.
REST methods return `HttpResult<T>` and websocket subscription methods return `WebSocketResult<UpdateSubscription>`. Always check `.Success`. `.Exchange` property identifies which exchange responded — useful for logging.
## Available shared interfaces
+1 -1
View File
@@ -30,7 +30,7 @@ For Binance-only code, use `BinanceRestClient` directly (see Binance.Net repo `A
## Result pattern
Every method returns `WebCallResult<T>` or `CallResult<T>`. Check `.Success` before `.Data`. `.Error` has structured info. `.Exchange` on shared clients identifies which exchange responded.
REST methods return `HttpResult<T>` and websocket subscription methods return `WebSocketResult<UpdateSubscription>`. Check `.Success` before `.Data`. `.Error` has structured info. `.Exchange` on shared clients identifies which exchange responded.
## Available shared interfaces
+1 -1
View File
@@ -85,7 +85,7 @@ Each exchange documents which interfaces it implements (some exchanges don't sup
## Core Pattern: Result Handling
Same as exchange-specific libraries`WebCallResult<T>` (REST) or `CallResult<T>` (WebSocket) with `.Success`, `.Data`, `.Error`. Always check `.Success` first.
Same as exchange-specific libraries: REST calls return `HttpResult<T>` and websocket subscription calls return `WebSocketResult<UpdateSubscription>`, both with `.Success`, `.Data`, and `.Error`. Always check `.Success` first.
```csharp
var result = await sharedClient.GetSpotTickerAsync(new GetTickerRequest(symbol));
+4 -120
View File
@@ -14,157 +14,41 @@ namespace CryptoExchange.Net.UnitTests
[Test]
public void TestBasicErrorCallResult()
{
var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown));
var result = CallResult.Fail(new ServerError("TestError", ErrorInfo.Unknown));
ClassicAssert.AreSame(result.Error!.ErrorCode, "TestError");
ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success);
}
[Test]
public void TestBasicSuccessCallResult()
{
var result = new CallResult(null);
var result = CallResult.Ok();
ClassicAssert.IsNull(result.Error);
Assert.That(result);
Assert.That(result.Success);
}
[Test]
public void TestCallResultError()
{
var result = new CallResult<object>(new ServerError("TestError", ErrorInfo.Unknown));
var result = CallResult.Fail<object>(new ServerError("TestError", ErrorInfo.Unknown));
ClassicAssert.AreSame(result.Error!.ErrorCode, "TestError");
ClassicAssert.IsNull(result.Data);
ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success);
}
[Test]
public void TestCallResultSuccess()
{
var result = new CallResult<object>(new object());
var result = CallResult.Ok<object>(new object());
ClassicAssert.IsNull(result.Error);
ClassicAssert.IsNotNull(result.Data);
Assert.That(result);
Assert.That(result.Success);
}
[Test]
public void TestCallResultSuccessAs()
{
var result = new CallResult<TestObjectResult>(new TestObjectResult());
var asResult = result.As<TestObject2>(result.Data.InnerData);
ClassicAssert.IsNull(asResult.Error);
ClassicAssert.IsNotNull(asResult.Data);
Assert.That(asResult.Data is not null);
Assert.That(asResult);
Assert.That(asResult.Success);
}
[Test]
public void TestCallResultErrorAs()
{
var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.As<TestObject2>(default);
ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError");
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
}
[Test]
public void TestCallResultErrorAsError()
{
var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError2");
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
}
[Test]
public void TestWebCallResultErrorAsError()
{
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError2");
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
}
[Test]
public void TestWebCallResultSuccessAsError()
{
var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK,
HttpVersion.Version11,
new HttpResponseMessage().Headers,
TimeSpan.FromSeconds(1),
null,
"{}",
1,
"https://test.com/api",
null,
HttpMethod.Get,
new HttpRequestMessage().Headers,
ResultDataSource.Server,
new TestObjectResult(),
null);
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error);
Assert.That(asResult.Error!.ErrorCode == "TestError2");
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
Assert.That(asResult.RequestUrl == "https://test.com/api");
Assert.That(asResult.RequestMethod == HttpMethod.Get);
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
}
[Test]
public void TestWebCallResultSuccessAsSuccess()
{
var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK,
HttpVersion.Version11,
new HttpResponseMessage().Headers,
TimeSpan.FromSeconds(1),
null,
"{}",
1,
"https://test.com/api",
null,
HttpMethod.Get,
new HttpRequestMessage().Headers,
ResultDataSource.Server,
new TestObjectResult(),
null);
var asResult = result.As<TestObject2>(result.Data.InnerData);
ClassicAssert.IsNull(asResult.Error);
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
Assert.That(asResult.RequestUrl == "https://test.com/api");
Assert.That(asResult.RequestMethod == HttpMethod.Get);
ClassicAssert.IsNotNull(asResult.Data);
Assert.That(asResult);
Assert.That(asResult.Success);
}
}
public class TestObjectResult
@@ -134,7 +134,7 @@ namespace CryptoExchange.Net.UnitTests.ClientTests
client.ApiClient1.SetParameterPosition(httpMethod, pos);
client.ApiClient1.SetNextResponse("{}", System.Net.HttpStatusCode.OK);
var result = await client.ApiClient1.GetResponseAsync<TestObject>(httpMethod, new ParameterCollection
var result = await client.ApiClient1.GetResponseAsync<TestObject>(httpMethod, new Parameters(new ParameterSerializationSettings())
{
{ "TestParam1", "Value1" },
{ "TestParam2", 2 },
@@ -110,7 +110,7 @@ namespace CryptoExchange.Net.UnitTests.ClientTests
var result = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => {}, false, default);
// act
await client.UnsubscribeAsync(result.Data);
await client.UnsubscribeAsync(result.Data!);
// assert
Assert.That(socket.Connected == false);
@@ -37,8 +37,8 @@ namespace CryptoExchange.Net.UnitTests
var symbols = CreateTestSymbols();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId, "Env", null);
// assert
Assert.That(hasCached, Is.True);
@@ -52,14 +52,14 @@ namespace CryptoExchange.Net.UnitTests
var symbols = CreateTestSymbols();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// assert
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCEUR"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHBTC"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "XRPUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "ETHUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCEUR"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "ETHBTC"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "XRPUSDT"), Is.True);
}
[Test]
@@ -78,13 +78,13 @@ namespace CryptoExchange.Net.UnitTests
};
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, initialSymbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, updatedSymbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, initialSymbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, updatedSymbols);
// assert - should still have only the initial symbol since less than 60 minutes passed
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCUSDT"), Is.True);
// The second update should not have been applied
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.False);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "ETHUSDT"), Is.False);
}
[Test]
@@ -95,8 +95,8 @@ namespace CryptoExchange.Net.UnitTests
var symbols = Array.Empty<SharedSpotSymbol>();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId, "Env", null);
// assert
Assert.That(hasCached, Is.False);
@@ -109,7 +109,7 @@ namespace CryptoExchange.Net.UnitTests
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.HasCached(nonExistentTopic);
var result = ExchangeSymbolCache.HasCached(nonExistentTopic, "Env", null);
// assert
Assert.That(result, Is.False);
@@ -121,10 +121,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeWithData";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.HasCached(topicId);
var result = ExchangeSymbolCache.HasCached(topicId, "Env", null);
// assert
Assert.That(result, Is.True);
@@ -135,10 +135,10 @@ namespace CryptoExchange.Net.UnitTests
{
// arrange
var topicId = "ExchangeNoData";
ExchangeSymbolCache.UpdateSymbolInfo(topicId, Array.Empty<SharedSpotSymbol>());
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, Array.Empty<SharedSpotSymbol>());
// act
var result = ExchangeSymbolCache.HasCached(topicId);
var result = ExchangeSymbolCache.HasCached(topicId, "Env", null);
// assert
Assert.That(result, Is.False);
@@ -150,10 +150,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeSupports";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT");
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "BTCUSDT");
// assert
Assert.That(result, Is.True);
@@ -165,10 +165,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeNoSupport";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "LINKUSDT");
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, "LINKUSDT");
// assert
Assert.That(result, Is.False);
@@ -181,7 +181,7 @@ namespace CryptoExchange.Net.UnitTests
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "BTCUSDT");
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "Env", null, "BTCUSDT");
// assert
Assert.That(result, Is.False);
@@ -193,11 +193,11 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeSharedSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol);
// assert
Assert.That(result, Is.True);
@@ -209,11 +209,11 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeNoSharedSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "LINK", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol);
// assert
Assert.That(result, Is.False);
@@ -225,11 +225,11 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeDifferentMode";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, sharedSymbol);
// assert
Assert.That(result, Is.False);
@@ -243,7 +243,7 @@ namespace CryptoExchange.Net.UnitTests
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, sharedSymbol);
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "Env", null, sharedSymbol);
// assert
Assert.That(result, Is.False);
@@ -255,10 +255,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeBaseAsset";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "BTC");
// assert
Assert.That(result, Is.Not.Null);
@@ -273,10 +273,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeCaseInsensitive";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "btc");
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "btc");
// assert
Assert.That(result, Is.Not.Null);
@@ -289,10 +289,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeNoBaseAsset";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "LINK");
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "LINK");
// assert
Assert.That(result, Is.Not.Null);
@@ -306,7 +306,7 @@ namespace CryptoExchange.Net.UnitTests
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(nonExistentTopic, "BTC");
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(nonExistentTopic, "Env", null, "BTC");
// assert
Assert.That(result, Is.Not.Null);
@@ -319,10 +319,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeParse";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "BTCUSDT");
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, "BTCUSDT");
// assert
Assert.That(result, Is.Not.Null);
@@ -338,10 +338,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeNoParse";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "LINKUSDT");
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, "LINKUSDT");
// assert
Assert.That(result, Is.Null);
@@ -353,10 +353,10 @@ namespace CryptoExchange.Net.UnitTests
// arrange
var topicId = "ExchangeNullSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, null);
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Env", null, null);
// assert
Assert.That(result, Is.Null);
@@ -369,7 +369,7 @@ namespace CryptoExchange.Net.UnitTests
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.ParseSymbol(nonExistentTopic, "BTCUSDT");
var result = ExchangeSymbolCache.ParseSymbol(nonExistentTopic, "Env", null, "BTCUSDT");
// assert
Assert.That(result, Is.Null);
@@ -391,14 +391,14 @@ namespace CryptoExchange.Net.UnitTests
};
// act
ExchangeSymbolCache.UpdateSymbolInfo(topic1, symbols1);
ExchangeSymbolCache.UpdateSymbolInfo(topic2, symbols2);
ExchangeSymbolCache.UpdateSymbolInfo(topic1, "Env", null, symbols1);
ExchangeSymbolCache.UpdateSymbolInfo(topic2, "Env", null, symbols2);
// assert
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "BTCUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "ETHUSDT"), Is.False);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "ETHUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "BTCUSDT"), Is.False);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "Env", null, "BTCUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "Env", null, "ETHUSDT"), Is.False);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "Env", null, "ETHUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "Env", null, "BTCUSDT"), Is.False);
}
[Test]
@@ -411,14 +411,14 @@ namespace CryptoExchange.Net.UnitTests
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, allSymbols);
// assert
var spotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var futuresSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, spotSymbol), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, futuresSymbol), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, spotSymbol), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "Env", null, futuresSymbol), Is.True);
}
[Test]
@@ -429,10 +429,10 @@ namespace CryptoExchange.Net.UnitTests
var spotSymbols = CreateTestSymbols();
var futuresSymbols = CreateFuturesSymbols();
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, allSymbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "BTC");
// assert
Assert.That(result.Length, Is.GreaterThanOrEqualTo(2));
@@ -451,15 +451,119 @@ namespace CryptoExchange.Net.UnitTests
new SharedSpotSymbol("ETH", "BTC", "ETHBTC", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "EUR", "ETHEUR", true, TradingMode.Spot)
};
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Env", null, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "ETH");
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Env", null, "ETH");
// assert
Assert.That(result.Length, Is.EqualTo(3));
Assert.That(result.All(x => x.BaseAsset == "ETH"), Is.True);
}
[Test]
public void GetSymbolsForBaseAsset_WithDifferentEnvironments_Should_ReturnNone()
{
// arrange
var topicId = "Topic1";
var spotSymbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", null, spotSymbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Test", null, "BTC");
// assert
Assert.That(result.Length, Is.EqualTo(0));
}
[Test]
public void GetSymbolsForBaseAsset_WithDifferentKey_Should_ReturnNone()
{
// arrange
var topicId = "Topic2";
var spotSymbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", "1", spotSymbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Live", "2", "BTC");
// assert
Assert.That(result.Length, Is.EqualTo(0));
}
[Test]
public void GetSymbolsForBaseAsset_WithSetKey_Should_ReturnNone()
{
// arrange
var topicId = "Topic3";
var spotSymbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", null, spotSymbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Live", "2", "BTC");
// assert
Assert.That(result.Length, Is.EqualTo(0));
}
[Test]
public void GetSymbolsForBaseAsset_WithNotSetKey_Should_ReturnNone()
{
// arrange
var topicId = "Topic4";
var spotSymbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", "2", spotSymbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "Live", null, "BTC");
// assert
Assert.That(result.Length, Is.EqualTo(2));
}
[Test]
public void ParseSymbol_WithDifferentKey_Should_ReturnNull()
{
// arrange
var topicId = "Topic5";
var spotSymbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", "1", spotSymbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Live", "2", "BTCUSDT");
// assert
Assert.That(result, Is.Null);
}
[Test]
public void ParseSymbol_WithSetKey_Should_ReturnNull()
{
// arrange
var topicId = "Topic6";
var spotSymbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", null, spotSymbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Live", "2", "BTCUSDT");
// assert
Assert.That(result, Is.Null);
}
[Test]
public void ParseSymbol_WithNotSetKey_Should_ReturnNull()
{
// arrange
var topicId = "Topic7";
var spotSymbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, "Live", "1", spotSymbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "Live", null, "BTCUSDT");
// assert
Assert.That(result, Is.Not.Null);
}
}
}
@@ -11,15 +11,15 @@ namespace CryptoExchange.Net.UnitTests.Implementations
{
public TestQuery(TestSocketMessage request, bool authenticated) : base(request, authenticated, 1)
{
MessageRouter = MessageRouter.CreateWithoutTopicFilter<TestSocketMessage>(request.Id.ToString(), HandleMessage);
MessageRouter = MessageRouter.CreateForQuery<TestSocketMessage>(request.Id.ToString(), HandleMessage);
}
private CallResult? HandleMessage(SocketConnection connection, DateTime time, string? arg3, TestSocketMessage message)
private CallResult<TestSocketMessage>? HandleMessage(SocketConnection connection, DateTime time, string? arg3, TestSocketMessage message)
{
if (message.Data != "OK")
return new CallResult(new ServerError(ErrorInfo.Unknown with { Message = message.Data }));
return CallResult.Fail<TestSocketMessage>(new ServerError(ErrorInfo.Unknown with { Message = message.Data }));
return CallResult.SuccessResult;
return CallResult.Ok(message);
}
}
}
@@ -20,8 +20,8 @@ namespace CryptoExchange.Net.UnitTests.Implementations
{
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
public TestRestApiClient(ILogger logger, HttpClient? httpClient, TestRestOptions options)
: base(logger, httpClient, options.Environment.RestClientAddress, options, options.ExchangeOptions)
public TestRestApiClient(ILoggerFactory? loggerFactory, HttpClient? httpClient, TestRestOptions options)
: base(loggerFactory, "Test", httpClient, options.Environment.RestClientAddress, options, options.ExchangeOptions)
{
}
@@ -48,14 +48,14 @@ namespace CryptoExchange.Net.UnitTests.Implementations
RequestFactory = factory;
}
internal async Task<WebCallResult<T>> GetResponseAsync<T>(HttpMethod? httpMethod = null, ParameterCollection? collection = null, RateLimitGate? rateLimitGate = null)
internal async Task<HttpResult<T>> GetResponseAsync<T>(HttpMethod? httpMethod = null, Parameters? collection = null, RateLimitGate? rateLimitGate = null)
{
var definition = new RequestDefinition("/path", httpMethod ?? HttpMethod.Get)
var definition = new RequestDefinition(BaseAddress, "/path", httpMethod ?? HttpMethod.Get)
{
Weight = rateLimitGate == null ? 0 : 1,
RateLimitGate = rateLimitGate
};
return await SendAsync<T>(BaseAddress, definition, collection ?? new ParameterCollection(), default);
return await SendAsync<T>(definition, collection ?? new Parameters(new ParameterSerializationSettings()), default);
}
internal void SetParameterPosition(HttpMethod httpMethod, HttpMethodParameterPosition pos)
@@ -20,8 +20,8 @@ namespace CryptoExchange.Net.UnitTests.Implementations
{
Initialize(options.Value);
ApiClient1 = AddApiClient(new TestRestApiClient(_logger, httpClient, options.Value));
ApiClient2 = AddApiClient(new TestRestApiClient(_logger, httpClient, options.Value));
ApiClient1 = AddApiClient(new TestRestApiClient(loggerFactory, httpClient, options.Value));
ApiClient2 = AddApiClient(new TestRestApiClient(loggerFactory, httpClient, options.Value));
}
}
}
@@ -1,4 +1,5 @@
using CryptoExchange.Net.UnitTests.ConverterTests;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.ConverterTests;
using CryptoExchange.Net.UnitTests.Implementations;
using System.Collections.Generic;
using System.Text.Json.Serialization;
@@ -11,6 +12,7 @@ namespace CryptoExchange.Net.UnitTests
[JsonSerializable(typeof(IDictionary<string, string>))]
[JsonSerializable(typeof(Dictionary<string, object>))]
[JsonSerializable(typeof(IDictionary<string, object>))]
[JsonSerializable(typeof(Parameters))]
[JsonSerializable(typeof(TestObject))]
[JsonSerializable(typeof(TestSocketMessage))]
@@ -17,13 +17,13 @@ namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestSocketApiClient : SocketApiClient<TestEnvironment, TestAuthenticationProvider, TestCredentials>
{
public TestSocketApiClient(ILogger logger, TestSocketOptions options)
: base(logger, options.Environment.SocketClientAddress, options, options.ExchangeOptions)
public TestSocketApiClient(ILoggerFactory? loggerFactory, TestSocketOptions options)
: base(loggerFactory, "Test", options.Environment.SocketClientAddress, options, options.ExchangeOptions)
{
}
public TestSocketApiClient(ILogger logger, HttpClient httpClient, string baseAddress, TestSocketOptions options, SocketApiOptions apiOptions)
: base(logger, baseAddress, options, apiOptions)
public TestSocketApiClient(ILoggerFactory? loggerFactory, HttpClient httpClient, string baseAddress, TestSocketOptions options, SocketApiOptions apiOptions)
: base(loggerFactory, "Test", baseAddress, options, apiOptions)
{
}
@@ -36,7 +36,7 @@ namespace CryptoExchange.Net.UnitTests.Implementations
protected override TestAuthenticationProvider CreateAuthenticationProvider(TestCredentials credentials) =>
new TestAuthenticationProvider(credentials);
public async Task<CallResult<UpdateSubscription>> SubscribeToUpdatesAsync<T>(Action<DataEvent<T>> handler, bool subQuery, CancellationToken ct)
public async Task<WebSocketResult<UpdateSubscription>> SubscribeToUpdatesAsync<T>(Action<DataEvent<T>> handler, bool subQuery, CancellationToken ct)
{
return await base.SubscribeAsync(new TestSubscription<T>(_logger, handler, subQuery, false), ct);
}
@@ -19,8 +19,8 @@ namespace CryptoExchange.Net.UnitTests.Implementations
{
Initialize(options.Value);
ApiClient1 = AddApiClient(new TestSocketApiClient(_logger, options.Value));
ApiClient2 = AddApiClient(new TestSocketApiClient(_logger, options.Value));
ApiClient1 = AddApiClient(new TestSocketApiClient(loggerFactory, options.Value));
ApiClient2 = AddApiClient(new TestSocketApiClient(loggerFactory, options.Value));
}
}
}
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.UnitTests.Implementations
_handler = handler;
_subQuery = subQuery;
MessageRouter = MessageRouter.CreateWithoutTopicFilter<T>("test", HandleUpdate);
MessageRouter = MessageRouter.CreateForEvent<T>("test", HandleUpdate);
}
protected override Query? GetSubQuery(SocketConnection connection)
@@ -44,7 +44,7 @@ namespace CryptoExchange.Net.UnitTests.Implementations
private CallResult? HandleUpdate(SocketConnection connection, DateTime time, string? originalData, T data)
{
_handler(new DataEvent<T>("Test", data, time, originalData));
return CallResult.SuccessResult;
return CallResult.Ok();
}
}
}
@@ -12,319 +12,248 @@ namespace CryptoExchange.Net.UnitTests
[Test]
public void AddingBasicValue_SetValueCorrectly()
{
var parameters = new ParameterCollection();
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", "value");
Assert.That(parameters["test"], Is.EqualTo("value"));
}
[Test]
public void AddingBasicNullValue_ThrowsException()
{
var parameters = new ParameterCollection();
Assert.Throws<ArgumentNullException>(() => parameters.Add("test", null!));
}
[Test]
public void AddingOptionalBasicValue_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptional("test", "value");
Assert.That(parameters["test"], Is.EqualTo("value"));
}
[Test]
public void AddingOptionalBasicNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptional("test", null);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingDecimalValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddString("test", 0.1m);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", 0.1m, DecimalSerialization.String);
Assert.That(parameters["test"], Is.EqualTo("0.1"));
}
[Test]
public void AddingOptionalDecimalValueAsString_SetValueCorrectly()
public void AddingDecimalValueAsString2_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", 0.1m);
var parameters = new Parameters(new ParameterSerializationSettings()
{
Decimal = DecimalSerialization.String
});
parameters.Add("test", 0.1m);
Assert.That(parameters["test"], Is.EqualTo("0.1"));
}
[Test]
public void AddingOptionalDecimalNullValueAsString_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", (decimal?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingIntValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddString("test", 1);
Assert.That(parameters["test"], Is.EqualTo("1"));
}
[Test]
public void AddingOptionalIntValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", 1);
Assert.That(parameters["test"], Is.EqualTo("1"));
}
[Test]
public void AddingOptionalIntNullValueAsString_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", (int?)null);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", (int?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingLongValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddString("test", 1L);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", 1L, IntegerSerialization.String);
Assert.That(parameters["test"], Is.EqualTo("1"));
}
[Test]
public void AddingOptionalLongValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", 1L);
var parameters = new Parameters(new ParameterSerializationSettings()
{
Integer = IntegerSerialization.String
});
parameters.Add("test", 1L);
Assert.That(parameters["test"], Is.EqualTo("1"));
}
[Test]
public void AddingOptionalLongNullValueAsString_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", (long?)null);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", (long?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingMillisecondTimestamp_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddMilliseconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTimeSerialization.MillisecondsNumber);
Assert.That(parameters["test"], Is.EqualTo(1735689600000));
}
[Test]
public void AddingOptionalMillisecondTimestamp_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalMilliseconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
var parameters = new Parameters(new ParameterSerializationSettings()
{
DateTimes = DateTimeSerialization.MillisecondsNumber
});
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
Assert.That(parameters["test"], Is.EqualTo(1735689600000));
}
[Test]
public void AddingOptionalMillisecondNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalMilliseconds("test", null);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", (DateTime?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingMillisecondTimestampString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddMillisecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTimeSerialization.MillisecondsString);
Assert.That(parameters["test"], Is.EqualTo("1735689600000"));
}
[Test]
public void AddingOptionalMillisecondTimestampString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalMillisecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
var parameters = new Parameters(new ParameterSerializationSettings()
{
DateTimes = DateTimeSerialization.MillisecondsString
});
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
Assert.That(parameters["test"], Is.EqualTo("1735689600000"));
}
[Test]
public void AddingOptionalMillisecondStringNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalMillisecondsString("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingSecondTimestamp_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddSeconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTimeSerialization.SecondsNumber);
Assert.That(parameters["test"], Is.EqualTo(1735689600));
}
[Test]
public void AddingOptionalSecondTimestamp_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalSeconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
var parameters = new Parameters(new ParameterSerializationSettings()
{
DateTimes = DateTimeSerialization.SecondsNumber
});
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
Assert.That(parameters["test"], Is.EqualTo(1735689600));
}
[Test]
public void AddingSecondNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalSeconds("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingSecondTimestampString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddSecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTimeSerialization.SecondsString);
Assert.That(parameters["test"], Is.EqualTo("1735689600"));
}
[Test]
public void AddingOptionalSecondTimestampString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalSecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
var parameters = new Parameters(new ParameterSerializationSettings()
{
DateTimes = DateTimeSerialization.SecondsString
});
parameters.Add("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
Assert.That(parameters["test"], Is.EqualTo("1735689600"));
}
[Test]
public void AddingSecondStringNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalSecondsString("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingEnum_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddEnum("test", TestEnum.Two);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", TestEnum.Two);
Assert.That(parameters["test"], Is.EqualTo("2"));
}
[Test]
public void AddingOptionalEnum_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalEnum("test", (TestEnum?)TestEnum.Two);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", (TestEnum?)TestEnum.Two);
Assert.That(parameters["test"], Is.EqualTo("2"));
}
[Test]
public void AddingOptionalEnumNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalEnum("test", (TestEnum?)null);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", (TestEnum?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingEnumAsInt_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddEnumAsInt("test", TestEnum.Two);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", TestEnum.Two, EnumSerialization.Number);
Assert.That(parameters["test"], Is.EqualTo(2));
}
[Test]
public void AddingOptionalEnumAsInt_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalEnumAsInt("test", (TestEnum?)TestEnum.Two);
var parameters = new Parameters(new ParameterSerializationSettings()
{
Enum = EnumSerialization.Number
});
parameters.Add("test", TestEnum.Two);
Assert.That(parameters["test"], Is.EqualTo(2));
}
[Test]
public void AddingOptionalEnumAsIntNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalEnumAsInt("test", (TestEnum?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingCommaSeparated_SetValueCorrectly()
{
var parameters = new ParameterCollection();
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.AddCommaSeparated("test", ["1", "2"]);
Assert.That(parameters["test"], Is.EqualTo("1,2"));
}
[Test]
public void AddingOptionalCommaSeparated_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalCommaSeparated("test", ["1", "2"]);
Assert.That(parameters["test"], Is.EqualTo("1,2"));
}
[Test]
public void AddingOptionalCommaSeparatedNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalCommaSeparated("test", (string[]?)null);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.AddCommaSeparated("test", (string[]?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingCommaSeparatedEnum_SetValueCorrectly()
{
var parameters = new ParameterCollection();
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.AddCommaSeparated("test", [TestEnum.Two, TestEnum.One]);
Assert.That(parameters["test"], Is.EqualTo("2,1"));
}
[Test]
public void AddingOptionalCommaSeparatedEnum_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalCommaSeparated("test", [TestEnum.Two, TestEnum.One]);
Assert.That(parameters["test"], Is.EqualTo("2,1"));
}
[Test]
public void AddingOptionalCommaSeparatedEnumNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalCommaSeparated("test", (TestEnum[]?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingBoolString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddBoolString("test", true);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", true, BoolSerialization.String);
Assert.That(parameters["test"], Is.EqualTo("true"));
}
[Test]
public void AddingOptionalBoolString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalBoolString("test", true);
var parameters = new Parameters(new ParameterSerializationSettings()
{
Bool = BoolSerialization.String
});
parameters.Add("test", true);
Assert.That(parameters["test"], Is.EqualTo("true"));
}
[Test]
public void AddingOptionalBoolStringNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalBoolString("test", null);
var parameters = new Parameters(new ParameterSerializationSettings());
parameters.Add("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
}
+42 -42
View File
@@ -29,16 +29,16 @@ namespace CryptoExchange.Net.UnitTests
var triggered = false;
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
var requestDefinition = new RequestDefinition("/sapi/v1/system/status", HttpMethod.Get);
var requestDefinition = new RequestDefinition("https://test.com", "/sapi/v1/system/status", HttpMethod.Get);
for (var i = 0; i < requests + 1; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(i == requests ? triggered : !triggered);
}
triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(!triggered);
}
@@ -52,13 +52,13 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
var requestDefinition = new RequestDefinition("https://test.com", endpoint, HttpMethod.Get);
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
bool expected = i == 1 ? expectLimiting ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
Assert.That(expected);
}
@@ -73,15 +73,15 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get);
var requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get);
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimiting ? evnt != null : evnt == null);
}
@@ -96,16 +96,16 @@ namespace CryptoExchange.Net.UnitTests
bool triggered = false;
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
var requestDefinition = new RequestDefinition("/sapi/test", HttpMethod.Get);
var requestDefinition = new RequestDefinition("https://test.com", "/sapi/test", HttpMethod.Get);
for (var i = 0; i < requests + 1; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(i == requests ? triggered : !triggered);
}
triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(!triggered);
}
@@ -117,13 +117,13 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
var requestDefinition = new RequestDefinition("https://test.com", endpoint, HttpMethod.Get);
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
Assert.That(expected);
}
@@ -137,13 +137,13 @@ namespace CryptoExchange.Net.UnitTests
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathsFilter(new[] { "/sapi/test", "/sapi/test2" }), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
var requestDefinition = new RequestDefinition("https://test.com", endpoint, HttpMethod.Get);
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++)
{
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
Assert.That(expected);
}
@@ -160,15 +160,15 @@ namespace CryptoExchange.Net.UnitTests
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
var requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, key1, 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, key2, 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
@@ -179,15 +179,15 @@ namespace CryptoExchange.Net.UnitTests
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, Array.Empty<IGuardFilter>(), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
var requestDefinition1 = new RequestDefinition("https://test.com", endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition("https://test.com", endpoint2, HttpMethod.Get) { Authenticated = true };
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, null, 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
@@ -199,15 +199,15 @@ namespace CryptoExchange.Net.UnitTests
{
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new HostFilter("https://test.com"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
var requestDefinition1 = new RequestDefinition(host1, endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(host2, endpoint2, HttpMethod.Get) { Authenticated = true };
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
@@ -222,9 +222,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition(host1, "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition(host2, "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(expectLimited ? evnt != null : evnt == null);
}
@@ -238,8 +238,8 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
}
@@ -250,16 +250,16 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerConnection, new LimitItemTypeFilter(RateLimitItemType.Request), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
var definition = new RequestDefinition("1", HttpMethod.Get) { ConnectionId = 1 };
var definition = new RequestDefinition("https://test.com", "1", HttpMethod.Get) { ConnectionId = 1 };
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
// act
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition, "https://test.com", null, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition, null, null, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
// assert
Assert.That(evnt, Is.Null);
@@ -272,17 +272,17 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerConnection, new LimitItemTypeFilter(RateLimitItemType.Request), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
var definition1 = new RequestDefinition("1", HttpMethod.Get) { ConnectionId = 1 };
var definition2 = new RequestDefinition("2", HttpMethod.Get) { ConnectionId = 2 };
var definition1 = new RequestDefinition("https://test.com", "1", HttpMethod.Get) { ConnectionId = 1 };
var definition2 = new RequestDefinition("https://test.com", "2", HttpMethod.Get) { ConnectionId = 2 };
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
// act
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition1, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default);
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition1, "https://test.com", null, null, default);
var result3 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition1, null, 1, RateLimitingBehaviour.Fail, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, default);
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition1, null, null, null, default);
var result3 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, default);
// assert
Assert.That(evnt, Is.Not.Null);
@@ -16,9 +16,9 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// arrange
var routes = new MessageRoute[]
{
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null),
MessageRoute<int>.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null)
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null),
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null),
MessageRoute.CreateForEvent<int>("type2", "topic2", (_, _, _, _) => null)
};
var router = new QueryRouter(routes);
@@ -46,10 +46,10 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
var collection = new QueryRouteCollection(typeof(string));
// act
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) => null));
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) => null));
var beforeMultipleReaders = collection.MultipleReaders;
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) => null, true));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) => null, true));
var afterMultipleReaders = collection.MultipleReaders;
// assert
@@ -63,12 +63,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// arrange
var calls = new List<string>();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
@@ -89,7 +89,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
// arrange
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("other-topic", MessageRoute<string>.CreateWithTopicFilter("type", "other-topic", (_, _, _, _) => null));
collection.AddRoute("other-topic", MessageRoute.CreateForEvent<string>("type", "other-topic", (_, _, _, _) => null));
collection.Build();
// act
@@ -106,12 +106,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// arrange
var calls = new List<string>();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
@@ -132,17 +132,17 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
// arrange
var calls = new List<string>();
var expectedResult = CallResult.SuccessResult;
var expectedResult = CallResult.Ok();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return expectedResult;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return new CallResult(null);
return CallResult.Ok();
}));
collection.Build();
@@ -160,17 +160,17 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
// arrange
var calls = new List<string>();
var expectedResult = CallResult.SuccessResult;
var expectedResult = CallResult.Ok();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return expectedResult;
}, true));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return new CallResult(null);
return CallResult.Ok();
}));
collection.Build();
@@ -188,22 +188,22 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
// arrange
var calls = new List<string>();
var expectedResult = CallResult.SuccessResult;
var expectedResult = CallResult.Ok();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return expectedResult;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("third");
return new CallResult(null);
return CallResult.Ok();
}));
collection.Build();
@@ -221,7 +221,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
// arrange
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) => null));
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) => null));
collection.Build();
// act
@@ -17,13 +17,13 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
var processor1 = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null)));
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null),
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null)));
var processor2 = new TestMessageProcessor(
2,
MessageRouter.Create(
MessageRoute<int>.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null)));
MessageRoute.CreateForEvent<int>("type2", "topic2", (_, _, _, _) => null)));
var table = new RoutingTable();
@@ -57,12 +57,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
var processor1 = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null)));
var processor2 = new TestMessageProcessor(
2,
MessageRouter.Create(
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null)));
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null)));
var table = new RoutingTable();
@@ -85,12 +85,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
var initialProcessor = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null)));
var replacementProcessor = new TestMessageProcessor(
2,
MessageRouter.Create(
MessageRoute<int>.CreateWithoutTopicFilter("type2", (_, _, _, _) => null)));
MessageRoute.CreateForEvent<int>("type2", (_, _, _, _) => null)));
var table = new RoutingTable();
table.Update(new IMessageProcessor[] { initialProcessor });
@@ -116,7 +116,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
var processor = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null)));
var table = new RoutingTable();
table.Update(new IMessageProcessor[] { processor });
@@ -15,9 +15,9 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// arrange
var routes = new MessageRoute[]
{
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null),
MessageRoute<int>.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null)
MessageRoute.CreateForEvent<string>("type1", (_, _, _, _) => null),
MessageRoute.CreateForEvent<string>("type1", "topic1", (_, _, _, _) => null),
MessageRoute.CreateForEvent<int>("type2", "topic2", (_, _, _, _) => null)
};
var router = new SubscriptionRouter(routes);
@@ -44,12 +44,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
@@ -61,7 +61,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(result, Is.SameAs(CallResult.Ok()));
Assert.That(calls, Is.EqualTo(new[] { "no-topic" }));
}
@@ -70,7 +70,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
// arrange
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute("other-topic", MessageRoute<string>.CreateWithTopicFilter("type", "other-topic", (_, _, _, _) => null));
collection.AddRoute("other-topic", MessageRoute.CreateForEvent<string>("type", "other-topic", (_, _, _, _) => null));
collection.Build();
// act
@@ -78,7 +78,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// assert
Assert.That(handled, Is.False);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(result, Is.SameAs(CallResult.Ok()));
}
[Test]
@@ -87,12 +87,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
collection.AddRoute(null, MessageRoute.CreateForEvent<string>("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
@@ -104,7 +104,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(result, Is.SameAs(CallResult.Ok()));
Assert.That(calls, Is.EqualTo(new[] { "no-topic", "topic" }));
}
@@ -114,12 +114,12 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return CallResult.SuccessResult;
return CallResult.Ok();
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return null;
@@ -131,7 +131,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(result, Is.SameAs(CallResult.Ok()));
Assert.That(calls, Is.EqualTo(new[] { "first", "second" }));
}
@@ -141,7 +141,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
collection.AddRoute("topic", MessageRoute.CreateForEvent<string>("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
@@ -153,7 +153,7 @@ namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
// assert
Assert.That(handled, Is.False);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(result, Is.SameAs(CallResult.Ok()));
Assert.That(calls, Is.Empty);
}
}
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.UnitTests
}
protected override Task<CallResult<bool>> DoResyncAsync(CancellationToken ct)
protected override Task<CallResult> DoResyncAsync(CancellationToken ct)
{
throw new NotImplementedException();
}
@@ -0,0 +1,347 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.TokenManagement;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NUnit.Framework;
using System;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
public class TokenManagementTests
{
private static readonly TimeSpan TestMaintenanceInterval = TimeSpan.FromMilliseconds(5);
[Test]
public async Task AcquireWithoutApiKeyReturnsCredentialsError()
{
var starts = 0;
var manager = CreateManager(
(_, _) =>
{
starts++;
return Task.FromResult(CallResult.Ok("token"));
});
var result = await manager.AcquireAsync(new TokenScope("Test", "Test", "Test", ""));
Assert.That(result.Success, Is.False);
Assert.That(result.Error, Is.TypeOf<NoApiCredentialsError>());
Assert.That(starts, Is.EqualTo(0));
}
[Test]
public async Task StartTokenFailureIsReturned()
{
var error = new ServerError(ErrorType.Unknown, "start failed");
var manager = CreateManager((_, _) => Task.FromResult(CallResult.Fail<string>(error)));
var result = await manager.AcquireAsync(CreateScope());
Assert.That(result.Success, Is.False);
Assert.That(result.Error, Is.SameAs(error));
}
[Test]
public async Task ActiveTokenIsSharedWhileLeasedAndStoppedAfterLastRelease()
{
var starts = 0;
var stops = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
stopToken: (_, _) =>
{
stops++;
return Task.FromResult(CallResult.Ok());
});
var scope = CreateScope();
var first = await manager.AcquireAsync(scope);
var second = await manager.AcquireAsync(scope);
AssertSuccess(first);
AssertSuccess(second);
Assert.That(second.Data!.Token.Token, Is.EqualTo(first.Data!.Token.Token));
Assert.That(starts, Is.EqualTo(1));
await first.Data!.ReleaseAsync();
Assert.That(stops, Is.EqualTo(0));
await second.Data!.ReleaseAsync();
Assert.That(stops, Is.EqualTo(1));
}
[Test]
public async Task ActiveTokenStartsNewTokenAfterLeaseRelease()
{
var starts = 0;
var stops = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
stopToken: (_, _) =>
{
stops++;
return Task.FromResult(CallResult.Ok());
});
var scope = CreateScope();
var first = await manager.AcquireAsync(scope);
AssertSuccess(first);
await first.Data!.ReleaseAsync();
var second = await manager.AcquireAsync(scope);
AssertSuccess(second);
Assert.That(second.Data!.Token.Token, Is.Not.EqualTo(first.Data!.Token.Token));
Assert.That(starts, Is.EqualTo(2));
Assert.That(stops, Is.EqualTo(1));
await second.Data!.ReleaseAsync();
}
[Test]
public async Task ReleasingLeaseTwiceOnlyStopsActiveTokenOnce()
{
var stops = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token")),
stopToken: (_, _) =>
{
stops++;
return Task.FromResult(CallResult.Ok());
});
var leaseResult = await manager.AcquireAsync(CreateScope());
AssertSuccess(leaseResult);
await leaseResult.Data!.ReleaseAsync();
await leaseResult.Data!.ReleaseAsync();
Assert.That(stops, Is.EqualTo(1));
}
[Test]
public async Task CachedTokenIsReusedAfterLeaseRelease()
{
var starts = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
managementType: TokenManagementType.Cached);
var scope = CreateScope();
var first = await manager.AcquireAsync(scope);
AssertSuccess(first);
await first.Data!.ReleaseAsync();
var second = await manager.AcquireAsync(scope);
AssertSuccess(second);
Assert.That(second.Data!.Token.Token, Is.EqualTo(first.Data!.Token.Token));
Assert.That(starts, Is.EqualTo(1));
await second.Data!.ReleaseAsync();
}
[Test]
public async Task CachedTokensAreScopedIndependently()
{
var starts = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
managementType: TokenManagementType.Cached);
var firstScope = CreateScope(additionalIdentifier: "one");
var secondScope = CreateScope(additionalIdentifier: "two");
var first = await manager.AcquireAsync(firstScope);
var second = await manager.AcquireAsync(secondScope);
AssertSuccess(first);
AssertSuccess(second);
await first.Data!.ReleaseAsync();
await second.Data!.ReleaseAsync();
var firstAgain = await manager.AcquireAsync(firstScope);
AssertSuccess(firstAgain);
Assert.That(firstAgain.Data!.Token.Token, Is.EqualTo(first.Data!.Token.Token));
Assert.That(second.Data!.Token.Token, Is.Not.EqualTo(first.Data!.Token.Token));
Assert.That(starts, Is.EqualTo(2));
await firstAgain.Data!.ReleaseAsync();
}
[Test]
public async Task ExpiredCachedTokenIsNotReused()
{
var starts = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
timeValid: TimeSpan.FromMilliseconds(20),
managementType: TokenManagementType.Cached);
var scope = CreateScope();
var first = await manager.AcquireAsync(scope);
AssertSuccess(first);
await first.Data!.ReleaseAsync();
await Task.Delay(50);
var second = await manager.AcquireAsync(scope);
AssertSuccess(second);
Assert.That(first.Data!.Token.Status, Is.EqualTo(TokenStatus.Expired));
Assert.That(second.Data!.Token.Token, Is.Not.EqualTo(first.Data!.Token.Token));
Assert.That(starts, Is.EqualTo(2));
await second.Data!.ReleaseAsync();
}
[Test]
public async Task CachedTokenDoesNotRunKeepAliveLoop()
{
var keepAlives = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token")),
refreshInterval: TimeSpan.FromMilliseconds(1),
keepAliveToken: (_, _) =>
{
keepAlives++;
return Task.FromResult(CallResult.Ok());
},
managementType: TokenManagementType.Cached);
var leaseResult = await manager.AcquireAsync(CreateScope());
AssertSuccess(leaseResult);
await Task.Delay(50);
Assert.That(keepAlives, Is.EqualTo(0));
await leaseResult.Data!.ReleaseAsync();
}
[Test]
public async Task ActiveTokenKeepAliveRefreshesValidity()
{
var keepAlives = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token")),
refreshInterval: TimeSpan.FromMilliseconds(1),
timeValid: TimeSpan.FromSeconds(1),
keepAliveToken: (_, _) =>
{
keepAlives++;
return Task.FromResult(CallResult.Ok());
});
var leaseResult = await manager.AcquireAsync(CreateScope());
AssertSuccess(leaseResult);
var originalValidUntil = leaseResult.Data!.Token.ValidUntil;
await WaitUntilAsync(() => keepAlives > 0);
Assert.That(leaseResult.Data!.Token.ValidUntil, Is.GreaterThan(originalValidUntil));
await leaseResult.Data!.ReleaseAsync();
}
[Test]
public async Task ActiveTokenKeepAliveFailureExpiresTokenWhenValidityPassed()
{
var starts = 0;
var expired = false;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
refreshInterval: TimeSpan.FromMilliseconds(1),
timeValid: TimeSpan.FromMilliseconds(25),
keepAliveToken: (_, _) => Task.FromResult(CallResult.Fail(new ServerError(ErrorType.Unknown, "keep alive failed"))));
var leaseResult = await manager.AcquireAsync(CreateScope());
AssertSuccess(leaseResult);
leaseResult.Data!.Token.Expired += _ => expired = true;
await WaitUntilAsync(() => expired);
Assert.That(leaseResult.Data!.Token.Status, Is.EqualTo(TokenStatus.Expired));
var nextLease = await manager.AcquireAsync(CreateScope());
AssertSuccess(nextLease);
Assert.That(nextLease.Data!.Token.Token, Is.Not.EqualTo(leaseResult.Data!.Token.Token));
Assert.That(starts, Is.EqualTo(2));
await leaseResult.Data!.ReleaseAsync();
await nextLease.Data!.ReleaseAsync();
}
[Test]
public async Task AcquireAndReplaceReleasesPreviousSubscriptionLease()
{
var starts = 0;
var stops = 0;
var manager = CreateManager(
(_, _) => Task.FromResult(CallResult.Ok("token-" + ++starts)),
stopToken: (_, _) =>
{
stops++;
return Task.FromResult(CallResult.Ok());
});
var subscription = new TestSubscription();
var first = await manager.AcquireAndReplaceAsync(subscription, CreateScope(additionalIdentifier: "one"));
AssertSuccess(first);
var second = await manager.AcquireAndReplaceAsync(subscription, CreateScope(additionalIdentifier: "two"));
AssertSuccess(second);
Assert.That(subscription.TokenLease, Is.SameAs(second.Data));
Assert.That(second.Data!.Token.Token, Is.Not.EqualTo(first.Data!.Token.Token));
Assert.That(stops, Is.EqualTo(1));
await subscription.TokenLease!.ReleaseAsync();
}
private static TokenManager CreateManager(
Func<TokenScope, System.Threading.CancellationToken, Task<CallResult<string>>> startToken,
TimeSpan? refreshInterval = null,
TimeSpan? timeValid = null,
Func<TokenInfo, System.Threading.CancellationToken, Task<CallResult>>? keepAliveToken = null,
Func<TokenInfo, System.Threading.CancellationToken, Task<CallResult>>? stopToken = null,
TokenManagementType managementType = TokenManagementType.Active)
{
return new TokenManager(
Guid.NewGuid().ToString(),
null,
refreshInterval ?? TimeSpan.FromMinutes(1),
timeValid ?? TimeSpan.FromMinutes(1),
startToken,
keepAliveToken,
stopToken,
managementType,
TestMaintenanceInterval);
}
private static TokenScope CreateScope(string apiKey = "apiKey", string? additionalIdentifier = null)
=> new TokenScope("Test", "Test", "Test", apiKey, additionalIdentifier);
private static void AssertSuccess(CallResult<TokenLease> result)
{
Assert.That(result.Success, Is.True, result.Error?.ToString());
Assert.That(result.Data, Is.Not.Null);
}
private static async Task WaitUntilAsync(Func<bool> condition)
{
var timeout = DateTime.UtcNow.AddSeconds(2);
while (!condition())
{
if (DateTime.UtcNow > timeout)
Assert.Fail("Condition was not met within the timeout");
await Task.Delay(10);
}
}
private sealed class TestSubscription : Subscription
{
public TestSubscription() : base(NullLogger.Instance, true)
{
}
protected override Query? GetSubQuery(SocketConnection connection) => null;
protected override Query? GetUnsubQuery(SocketConnection connection) => null;
}
}
}
@@ -1,7 +1,7 @@
#if NETSTANDARD2_0
namespace System.Diagnostics.CodeAnalysis
namespace System.Diagnostics.CodeAnalysis
{
using System;
#if NETSTANDARD2_0
/// <summary>
/// Specifies that <see langword="null"/> is allowed as an input even if the
@@ -206,5 +206,26 @@ namespace System.Diagnostics.CodeAnalysis
ReturnValue = returnValue;
}
}
#endif
#if NETSTANDARD2_0 || NETSTANDARD2_1
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = false)]
[ExcludeFromCodeCoverage]
internal sealed class MemberNotNullWhenAttribute : Attribute
{
public MemberNotNullWhenAttribute(bool returnValue, string member)
{
ReturnValue = returnValue;
Members = [member];
}
public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
{
ReturnValue = returnValue;
Members = members;
}
public bool ReturnValue { get; }
public string[] Members { get; }
}
#endif
}
#endif
@@ -439,13 +439,13 @@ namespace CryptoExchange.Net.Authentication
/// <param name="serializer"></param>
/// <param name="parameters"></param>
/// <returns></returns>
protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
protected static string GetSerializedBody(IMessageSerializer serializer, Parameters? parameters)
{
if (serializer is not IStringMessageSerializer stringSerializer)
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
if (parameters?.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
return stringSerializer.Serialize(value);
if (parameters?.BodyValue != null)
return stringSerializer.Serialize(parameters.BodyValue);
else
return stringSerializer.Serialize(parameters);
}
+27 -6
View File
@@ -4,6 +4,7 @@ using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace CryptoExchange.Net.Clients
{
@@ -25,7 +26,7 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// If we are disposing
/// </summary>
protected bool _disposing;
protected bool _disposed;
/// <summary>
/// Whether a proxy is configured
@@ -47,6 +48,11 @@ namespace CryptoExchange.Net.Clients
}
}
/// <summary>
/// The name of the exchange this client is for
/// </summary>
public string Exchange { get; }
/// <summary>
/// The environment this client communicates to
/// </summary>
@@ -75,20 +81,26 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="exchange">The exchange name</param>
/// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
/// <param name="baseAddress">Base address for this API client</param>
/// <param name="clientOptions">Client options</param>
/// <param name="apiOptions">Api options</param>
protected BaseApiClient(
ILogger logger,
ILoggerFactory? loggerFactory,
string exchange,
bool outputOriginalData,
string baseAddress,
ExchangeOptions clientOptions,
ApiOptions apiOptions)
{
_logger = logger;
var loggerName = ClientName.StartsWith(exchange, StringComparison.OrdinalIgnoreCase)
? exchange + "." + ClientName.Substring(exchange.Length).TrimStart('.')
: exchange + "." + ClientName;
_logger = loggerFactory?.CreateLogger(loggerName) ?? NullLogger.Instance;
Exchange = exchange;
ClientOptions = clientOptions;
ApiOptions = apiOptions;
OutputOriginalData = outputOriginalData;
@@ -113,9 +125,18 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose()
public void Dispose()
{
_disposing = true;
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose
/// </summary>
protected virtual void Dispose(bool disposing)
{
_disposed = true;
}
}
}
+7 -1
View File
@@ -1,6 +1,7 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Threading;
@@ -89,7 +90,6 @@ namespace CryptoExchange.Net.Clients
throw new ArgumentNullException(nameof(options));
ClientOptions = options;
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
}
/// <summary>
@@ -115,6 +115,12 @@ namespace CryptoExchange.Net.Clients
return opts;
}
/// <inheritdoc />
public override string ToString()
{
return $"{GetType().Name}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}, configuration: {ClientOptions}";
}
/// <summary>
/// Dispose
/// </summary>
+1 -1
View File
@@ -72,7 +72,7 @@ namespace CryptoExchange.Net.Clients
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
public void SetApiCredentials(TApiCredentials credentials)
public virtual void SetApiCredentials(TApiCredentials credentials)
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
+127 -135
View File
@@ -41,11 +41,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
/// <summary>
/// How to serialize array parameters when making requests
/// </summary>
protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
/// <summary>
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
/// </summary>
@@ -56,16 +51,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected Dictionary<string, string> StandardRequestHeaders { get; set; } = [];
/// <summary>
/// Whether parameters need to be ordered
/// </summary>
protected internal bool OrderParameters { get; set; } = true;
/// <summary>
/// Parameter order comparer
/// </summary>
protected IComparer<string> ParameterOrderComparer { get; } = new OrderedStringComparer();
/// <summary>
/// Where to put the parameters for requests with different Http methods
/// </summary>
@@ -108,7 +93,7 @@ namespace CryptoExchange.Net.Clients
/// Get the AuthenticationProvider implementation, or null if no ApiCredentials are set
/// </summary>
public virtual AuthenticationProvider? GetAuthenticationProvider() => null;
/// <summary>
/// Configured environment name
/// </summary>
@@ -117,17 +102,20 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="exchangeName">The exchange name</param>
/// <param name="httpClient">HttpClient to use</param>
/// <param name="baseAddress">Base address for this API client</param>
/// <param name="options">The base client options</param>
/// <param name="apiOptions">The Api client options</param>
public RestApiClient(ILogger logger,
public RestApiClient(ILoggerFactory? loggerFactory,
string exchangeName,
HttpClient? httpClient,
string baseAddress,
RestExchangeOptions options,
RestApiOptions apiOptions)
: base(logger,
: base(loggerFactory,
exchangeName,
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
baseAddress,
options,
@@ -144,33 +132,10 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
protected abstract IMessageSerializer CreateSerializer();
/// <summary>
/// Send a request to the base address based on the request definition
/// </summary>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="parameters">Request parameters</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <returns></returns>
protected virtual async Task<WebCallResult> SendAsync(
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null)
{
var result = await SendAsync<object>(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
return result.AsDataless();
}
/// <summary>
/// Send a request to the base address based on the request definition
/// </summary>
/// <typeparam name="T">Response type</typeparam>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="parameters">Request parameters</param>
/// <param name="cancellationToken">Cancellation token</param>
@@ -179,10 +144,9 @@ namespace CryptoExchange.Net.Clients
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns>
protected virtual Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
protected virtual Task<HttpResult<T>> SendAsync<T>(
RequestDefinition definition,
ParameterCollection? parameters,
Parameters? parameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null,
@@ -191,7 +155,6 @@ namespace CryptoExchange.Net.Clients
{
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
return SendAsync<T>(
baseAddress,
definition,
parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null,
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
@@ -206,7 +169,6 @@ namespace CryptoExchange.Net.Clients
/// Send a request to the base address based on the request definition
/// </summary>
/// <typeparam name="T">Response type</typeparam>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="uriParameters">Request query parameters</param>
/// <param name="bodyParameters">Request body parameters</param>
@@ -216,11 +178,10 @@ namespace CryptoExchange.Net.Clients
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
protected virtual async Task<HttpResult<T>> SendAsync<T>(
RequestDefinition definition,
ParameterCollection? uriParameters,
ParameterCollection? bodyParameters,
Parameters? uriParameters,
Parameters? bodyParameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null,
@@ -231,20 +192,20 @@ namespace CryptoExchange.Net.Clients
if (definition.Authenticated && GetAuthenticationProvider() == null)
{
_logger.RestApiNoApiCredentials(requestId, definition.Path);
return new WebCallResult<T>(new NoApiCredentialsError());
return HttpResult.Fail<T>(Exchange, new NoApiCredentialsError());
}
string? cacheKey = null;
if (ShouldCache(definition))
{
cacheKey = baseAddress + definition + uriParameters?.ToFormData();
cacheKey = definition.FullUrl + definition + uriParameters?.ToFormData();
_logger.CheckingCache(cacheKey);
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
if (cachedValue != null)
{
_logger.CacheHit(cacheKey);
var original = (WebCallResult<T>)cachedValue;
return original.Cached();
var original = (HttpResult<T>)cachedValue;
return original with { DataSource = ResultDataSource.Cache };
}
_logger.CacheNotHit(cacheKey);
@@ -258,7 +219,6 @@ namespace CryptoExchange.Net.Clients
await CheckTimeSync(requestId, definition).ConfigureAwait(false);
var error = await RateLimitAsync(
baseAddress,
requestId,
definition,
weight ?? definition.Weight,
@@ -266,11 +226,10 @@ namespace CryptoExchange.Net.Clients
weightSingleLimiter,
rateLimitKeySuffix).ConfigureAwait(false);
if (error != null)
return new WebCallResult<T>(error);
return HttpResult.Fail<T>(Exchange, error);
var request = CreateRequest(
requestId,
baseAddress,
definition,
uriParameters,
bodyParameters,
@@ -284,7 +243,7 @@ namespace CryptoExchange.Net.Clients
if (result.Error is not CancellationRequestedError)
{
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
if (!result)
if (!result.Success)
{
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception);
}
@@ -316,7 +275,6 @@ namespace CryptoExchange.Net.Clients
/// Check rate limits for the request
/// </summary>
protected virtual async ValueTask<Error?> RateLimitAsync(
string host,
int requestId,
RequestDefinition definition,
int weight,
@@ -338,13 +296,12 @@ namespace CryptoExchange.Net.Clients
requestId,
RateLimitItemType.Request,
definition,
host,
GetAuthenticationProvider()?.Key,
requestWeight,
requestWeight,
ClientOptions.RateLimitingBehaviour,
rateLimitKeySuffix + ClientOptions.RateLimitGroup,
cancellationToken).ConfigureAwait(false);
if (!limitResult)
if (!limitResult.Success)
return limitResult.Error!;
}
}
@@ -360,17 +317,16 @@ namespace CryptoExchange.Net.Clients
var singleRequestWeight = weightSingleLimiter ?? 1;
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(
_logger,
requestId,
requestId,
definition.LimitGuard,
RateLimitItemType.Request,
definition,
host,
GetAuthenticationProvider()?.Key,
singleRequestWeight,
ClientOptions.RateLimitingBehaviour,
rateLimitKeySuffix,
cancellationToken).ConfigureAwait(false);
if (!limitResult)
if (!limitResult.Success)
return limitResult.Error!;
}
}
@@ -382,7 +338,6 @@ namespace CryptoExchange.Net.Clients
/// Creates a request object
/// </summary>
/// <param name="requestId">Id of the request</param>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="uriParameters">The query parameters of the request</param>
/// <param name="bodyParameters">The body parameters of the request</param>
@@ -390,19 +345,16 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
protected virtual IRequest CreateRequest(
int requestId,
string baseAddress,
RequestDefinition definition,
ParameterCollection? uriParameters,
ParameterCollection? bodyParameters,
Parameters? uriParameters,
Parameters? bodyParameters,
Dictionary<string, string>? additionalHeaders)
{
var requestConfiguration = new RestRequestConfiguration(
definition,
baseAddress,
uriParameters == null ? null : CreateParameterDictionary(uriParameters),
bodyParameters == null ? null : CreateParameterDictionary(bodyParameters),
uriParameters,
bodyParameters,
additionalHeaders,
definition.ArraySerialization ?? ArraySerialization,
definition.ParameterPosition ?? ParameterPositions[definition.Method],
definition.RequestBodyFormat ?? RequestBodyFormat);
@@ -414,20 +366,16 @@ namespace CryptoExchange.Net.Clients
{
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
}
var queryString = requestConfiguration.GetQueryString(true);
if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?"))
queryString = $"?{queryString}";
var path = baseAddress.AppendPath(definition.Path);
if (definition.ForcePathEndWithSlash == true && !path.EndsWith("/"))
path += "/";
var uri = new Uri(path + queryString);
var uri = new Uri(definition.FullUrl + queryString);
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
request.Accept = MessageHandler.AcceptHeader;
if (requestConfiguration.Headers != null)
if (requestConfiguration.Headers != null)
{
foreach (var header in requestConfiguration.Headers)
request.AddHeader(header.Key, header.Value);
@@ -436,10 +384,12 @@ namespace CryptoExchange.Net.Clients
foreach (var header in StandardRequestHeaders)
{
// Only add it if it isn't overwritten
requestConfiguration.Headers ??= new Dictionary<string, string>();
if (!requestConfiguration.Headers.ContainsKey(header.Key))
if (requestConfiguration.Headers == null
|| !requestConfiguration.Headers.ContainsKey(header.Key))
{
request.AddHeader(header.Key, header.Value);
}
}
}
if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody)
{
@@ -451,10 +401,10 @@ namespace CryptoExchange.Net.Clients
}
else
{
if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0)
if (requestConfiguration.BodyParameters != null && !requestConfiguration.BodyParameters.Empty)
WriteParamBody(request, requestConfiguration.BodyParameters, contentType);
else if (OmitContentTypeHeaderWithoutContent != true)
request.SetContent(RequestBodyEmptyContent, RequestBodyContentEncoding, contentType);
request.SetContent(RequestBodyEmptyContent, RequestBodyContentEncoding, contentType);
}
}
@@ -469,7 +419,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="gate">The ratelimit gate used</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> GetResponseAsync2<T>(
protected virtual async Task<HttpResult<T>> GetResponseAsync2<T>(
RequestDefinition requestDefinition,
IRequest request,
IRateLimitGate? gate,
@@ -535,16 +485,16 @@ namespace CryptoExchange.Net.Clients
{
_logger.LogError(ex, "Unhandled exception when parsing error response: {Message}", ex.Message);
var errorResult = new ServerError(ErrorInfo.Unknown with { Message = ex.Message });
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, errorResult);
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, errorResult);
}
}
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, error);
}
if (typeof(T) == typeof(object))
if (typeof(T) == Unit.Type)
// Success status code and expected empty response, assume it's correct
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
return OkHttpRequest<T>(request, response, sw.Elapsed, originalData, default!);
// Data response received, inspect the message and check if it is an error or not
var parsedError = await MessageHandler.CheckForErrorResponse(
@@ -563,7 +513,7 @@ namespace CryptoExchange.Net.Clients
}
// Success status code, but TryParseError determined it was an error response
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, parsedError);
}
if (MessageHandler.RequiresSeekableStream)
@@ -571,45 +521,45 @@ namespace CryptoExchange.Net.Clients
responseStream.Position = 0;
// Try deserialization into the expected type
var (deserializeResult, deserializeError) = await MessageHandler.TryDeserializeAsync<T>(responseStream, cancellationToken).ConfigureAwait(false);
var (deserializeResult, deserializeError) = await MessageHandler.TryDeserializeAsync<T>(responseStream, cancellationToken).ConfigureAwait(false);
if (deserializeError != null)
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, deserializeError); ;
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, deserializeError, deserializeResult);
try
{
// Check the deserialized response to see if it's an error or not
var responseError = MessageHandler.CheckDeserializedResponse(response.ResponseHeaders, deserializeResult);
if (responseError != null)
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, responseError);
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, responseError, deserializeResult);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception when checking deserialized response: {Message}", ex.Message);
var error = new ServerError(ErrorInfo.Unknown with { Message = ex.Message });
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, error);
return FailHttpRequest<T>(request, response, sw.Elapsed, originalData, error, deserializeResult);
}
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, null);
return OkHttpRequest<T>(request, response, sw.Elapsed, originalData, deserializeResult!);
}
catch (HttpRequestException requestException)
{
// Request exception, can't reach server for instance
var error = new WebError(requestException.Message, requestException);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
return FailHttpRequest<T>(request, response, sw.Elapsed, null, error);
}
catch (OperationCanceledException canceledException)
{
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
{
// Cancellation token canceled by caller
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException));
return FailHttpRequest<T>(request, null, sw.Elapsed, null, new CancellationRequestedError(canceledException));
}
else
{
// Request timed out
var error = new WebError($"Request timed out", exception: canceledException);
error.ErrorType = ErrorType.Timeout;
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
return FailHttpRequest<T>(request, null, sw.Elapsed, null, error);
}
}
catch (ArgumentException argumentException)
@@ -618,7 +568,7 @@ namespace CryptoExchange.Net.Clients
{
// Unsupported HTTP version error .net framework
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + argumentException.Message);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
return FailHttpRequest<T>(request, null, sw.Elapsed, null, error);
}
throw;
@@ -629,7 +579,7 @@ namespace CryptoExchange.Net.Clients
{
// Unsupported HTTP version error dotnet code
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + notSupportedException.Message);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
return FailHttpRequest<T>(request, null, sw.Elapsed, null, error);
}
throw;
@@ -641,16 +591,55 @@ namespace CryptoExchange.Net.Clients
}
}
private HttpResult<T> OkHttpRequest<T>(IRequest request, IResponse response, TimeSpan elapsed, string? originalData, T result)
{
return HttpResult.Ok(
Exchange,
response.StatusCode,
response.HttpVersion,
response.ResponseHeaders,
elapsed,
response.ContentLength,
originalData,
request.RequestId,
request.Uri.ToString(),
request.Content,
request.Method,
request.GetHeaders(),
ResultDataSource.Server,
result);
}
private HttpResult<T> FailHttpRequest<T>(IRequest request, IResponse? response, TimeSpan elapsed, string? originalData, Error error, T? result = default)
{
return HttpResult.Fail<T>(
Exchange,
response?.StatusCode,
response?.HttpVersion,
response?.ResponseHeaders,
elapsed,
response?.ContentLength,
originalData,
request.RequestId,
request.Uri.ToString(),
request.Content,
request.Method,
request.GetHeaders(),
ResultDataSource.Server,
error,
result);
}
/// <summary>
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
/// Note that this is always called; even when the request might be successful
/// </summary>
/// <typeparam name="T">WebCallResult type parameter</typeparam>
/// <typeparam name="T">HttpResult type parameter</typeparam>
/// <param name="gate">The rate limit gate the call used</param>
/// <param name="callResult">The result of the call</param>
/// <param name="tries">The current try number</param>
/// <returns>True if call should retry, false if the call should return</returns>
protected virtual async ValueTask<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, WebCallResult<T> callResult, int tries)
protected virtual async ValueTask<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, HttpResult<T> callResult, int tries)
{
if (tries >= 2)
// Only retry once
@@ -681,7 +670,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="request">The request to set the parameters on</param>
/// <param name="parameters">The parameters to set</param>
/// <param name="contentType">The content type of the data</param>
protected virtual void WriteParamBody(IRequest request, IDictionary<string, object> parameters, string contentType)
protected virtual void WriteParamBody(IRequest request, Parameters parameters, string contentType)
{
if (contentType == Constants.JsonContentHeader)
{
@@ -691,8 +680,13 @@ namespace CryptoExchange.Net.Clients
// Write the parameters as json in the body
string stringData;
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
stringData = stringSerializer.Serialize(value);
if (parameters.BodyValue != null)
{
if (parameters.BodyValue is string bodyString)
stringData = bodyString;
else
stringData = stringSerializer.Serialize(parameters.BodyValue);
}
else
stringData = stringSerializer.Serialize(parameters);
request.SetContent(stringData, RequestBodyContentEncoding, contentType);
@@ -705,24 +699,11 @@ namespace CryptoExchange.Net.Clients
}
}
/// <summary>
/// Create the parameter IDictionary
/// </summary>
/// <param name="parameters"></param>
/// <returns></returns>
protected internal IDictionary<string, object> CreateParameterDictionary(IDictionary<string, object> parameters)
{
if (!OrderParameters)
return parameters;
return new SortedDictionary<string, object>(parameters, ParameterOrderComparer);
}
/// <summary>
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
/// </summary>
/// <returns>Server time</returns>
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
protected virtual Task<HttpResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
private async ValueTask CheckTimeSync(int requestId, RequestDefinition definition)
{
@@ -757,7 +738,7 @@ namespace CryptoExchange.Net.Clients
return;
var localTime = DateTime.UtcNow;
WebCallResult<DateTime> result;
HttpResult<DateTime> result;
try
{
result = await GetServerTimestampAsync().ConfigureAwait(false);
@@ -767,7 +748,7 @@ namespace CryptoExchange.Net.Clients
throw new ArgumentException("AutoTimestamp is not available for this API");
}
if (!result)
if (!result.Success)
{
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
return;
@@ -778,7 +759,7 @@ namespace CryptoExchange.Net.Clients
// If this was the first request make another one to calculate the offset since the first one can be slower
localTime = DateTime.UtcNow;
result = await GetServerTimestampAsync().ConfigureAwait(false);
if (!result)
if (!result.Success)
{
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail");
return;
@@ -845,12 +826,14 @@ namespace CryptoExchange.Net.Clients
/// ctor
/// </summary>
protected RestApiClient(
ILogger logger,
ILoggerFactory? loggerFactory,
string exchangeName,
HttpClient? httpClient,
string baseAddress,
RestExchangeOptions options,
RestApiOptions apiOptions) : base(
logger,
loggerFactory,
exchangeName,
httpClient,
baseAddress,
options,
@@ -877,12 +860,14 @@ namespace CryptoExchange.Net.Clients
/// ctor
/// </summary>
protected RestApiClient(
ILogger logger,
ILoggerFactory? loggerFactory,
string exchangeName,
HttpClient? httpClient,
string baseAddress,
RestExchangeOptions<TEnvironment, TApiCredentials> options,
RestApiOptions apiOptions) : base(
logger,
loggerFactory,
exchangeName,
httpClient,
baseAddress,
options,
@@ -912,13 +897,18 @@ namespace CryptoExchange.Net.Clients
where TAuthenticationProvider : AuthenticationProvider<TApiCredentials>
where TEnvironment : TradeEnvironment
{
private bool _authProviderInitialized = false;
private TAuthenticationProvider? _authenticationProvider;
/// <summary>
/// Auth provider initialized field
/// </summary>
protected bool _authProviderInitialized = false;
/// <summary>
/// Auth provider field
/// </summary>
protected TAuthenticationProvider? _authenticationProvider;
/// <summary>
/// The authentication provider for this API client. (null if no credentials are set)
/// </summary>
public TAuthenticationProvider? AuthenticationProvider
public virtual TAuthenticationProvider? AuthenticationProvider
{
get
{
@@ -932,7 +922,7 @@ namespace CryptoExchange.Net.Clients
return _authenticationProvider;
}
internal set => _authenticationProvider = value;
protected internal set => _authenticationProvider = value;
}
/// <inheritdoc />
@@ -942,12 +932,14 @@ namespace CryptoExchange.Net.Clients
/// ctor
/// </summary>
protected RestApiClient(
ILogger logger,
ILoggerFactory? loggerFactory,
string exchangeName,
HttpClient? httpClient,
string baseAddress,
RestExchangeOptions<TEnvironment, TApiCredentials> options,
RestApiOptions apiOptions) : base(
logger,
loggerFactory,
exchangeName,
httpClient,
baseAddress,
options,
+206 -166
View File
@@ -15,6 +15,7 @@ using CryptoExchange.Net.Sockets.Default.Interfaces;
using CryptoExchange.Net.Sockets.HighPerf;
using CryptoExchange.Net.Sockets.HighPerf.Interfaces;
using CryptoExchange.Net.Sockets.Interfaces;
using CryptoExchange.Net.TokenManagement;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
@@ -71,11 +72,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
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>
/// The rate limiters
/// </summary>
@@ -153,21 +149,26 @@ namespace CryptoExchange.Net.Clients
/// Configured environment name
/// </summary>
public abstract string EnvironmentName { get; }
private int _isDisposed;
#endregion
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">log</param>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="exchangeName">Exchange name</param>
/// <param name="options">Client options</param>
/// <param name="baseAddress">Base address for this API client</param>
/// <param name="apiOptions">The Api client options</param>
public SocketApiClient(
ILogger logger,
ILoggerFactory? loggerFactory,
string exchangeName,
string baseAddress,
SocketExchangeOptions options,
SocketApiOptions apiOptions)
: base(logger,
: base(loggerFactory,
exchangeName,
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
baseAddress,
options,
@@ -216,7 +217,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="interval"></param>
/// <param name="queryDelegate"></param>
/// <param name="callback"></param>
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<ISocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<ISocketConnection, Query> queryDelegate, Action<SocketConnection, WebSocketResult>? callback)
{
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
{
@@ -233,7 +234,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="subscription">The subscription</param>
/// <param name="ct">Cancellation token for closing this subscription</param>
/// <returns></returns>
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync(Subscription subscription, CancellationToken ct)
protected virtual Task<WebSocketResult<UpdateSubscription>> SubscribeAsync(Subscription subscription, CancellationToken ct)
{
return SubscribeAsync(BaseAddress, subscription, ct);
}
@@ -245,86 +246,102 @@ namespace CryptoExchange.Net.Clients
/// <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(string url, Subscription subscription, CancellationToken ct)
protected virtual async Task<WebSocketResult<UpdateSubscription>> SubscribeAsync(string url, Subscription subscription, CancellationToken ct)
{
if (_disposing)
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
if (subscription.Authenticated && GetAuthenticationProvider() == null)
{
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
}
if (subscription.IndividualSubscriptionCount > MaxIndividualSubscriptionsPerConnection)
return new CallResult<UpdateSubscription>(ArgumentError.Invalid("subscriptions", $"Max number of subscriptions in a single call is {MaxIndividualSubscriptionsPerConnection}"));
SocketConnection socketConnection;
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
bool successResult = false;
try
{
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
}
catch (OperationCanceledException tce)
{
return new CallResult<UpdateSubscription>(new CancellationRequestedError(tce));
}
if (_disposed)
return WebSocketResult.Fail<UpdateSubscription>(Exchange, new InvalidOperationError("Client disposed, can't subscribe"));
try
{
while (true)
if (subscription.Authenticated && GetAuthenticationProvider() == null)
{
// Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<UpdateSubscription>(null);
socketConnection = socketResult.Data;
// Add a subscription on the socket connection
var success = socketConnection.AddSubscription(subscription);
if (!success)
{
_logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId);
continue;
}
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
{
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
semaphoreSlim.Release();
released = true;
}
var needsConnecting = !socketConnection.Connected;
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false);
if (!connectResult)
return new CallResult<UpdateSubscription>(connectResult.Error!);
break;
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
return WebSocketResult.Fail<UpdateSubscription>(Exchange, new NoApiCredentialsError());
}
if (subscription.IndividualSubscriptionCount > MaxIndividualSubscriptionsPerConnection)
return WebSocketResult.Fail<UpdateSubscription>(Exchange, ArgumentError.Invalid("subscriptions", $"Max number of subscriptions in a single call is {MaxIndividualSubscriptionsPerConnection}"));
SocketConnection socketConnection;
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
try
{
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
}
catch (OperationCanceledException tce)
{
return WebSocketResult.Fail<UpdateSubscription>(Exchange, new CancellationRequestedError(tce));
}
try
{
while (true)
{
// Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
if (!socketResult.Success)
return WebSocketResult.Fail<UpdateSubscription>(Exchange, socketResult.Error);
socketConnection = socketResult.Data;
// Add a subscription on the socket connection
var success = socketConnection.AddSubscription(subscription);
if (!success)
{
_logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId);
continue;
}
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
{
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
semaphoreSlim.Release();
released = true;
}
var needsConnecting = !socketConnection.Connected;
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false);
if (!connectResult.Success)
return WebSocketResult.Fail<UpdateSubscription>(Exchange, connectResult.Error!);
break;
}
}
finally
{
if (!released)
semaphoreSlim.Release();
}
if (socketConnection.PausedActivity)
{
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
return WebSocketResult.Fail<UpdateSubscription>(Exchange, new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
}
var subscribeResult = await socketConnection.TrySubscribeAsync(subscription, true, ct).ConfigureAwait(false);
if (!subscribeResult.Success)
return WebSocketResult.Fail<UpdateSubscription>(Exchange, subscribeResult.Error!);
successResult = true;
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
return WebSocketResult.Ok(
Exchange,
socketConnection.SocketId,
subscribeResult.ResponseTime!.Value,
subscribeResult.RequestId!.Value,
subscribeResult.Url,
new UpdateSubscription(socketConnection, subscription));
}
finally
{
if (!released)
semaphoreSlim.Release();
if (!successResult && subscription.TokenLease != null)
_ = subscription.TokenLease.ReleaseAsync();
}
if (socketConnection.PausedActivity)
{
_logger.HasBeenPausedCantSubscribeAtThisMoment(socketConnection.SocketId);
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
}
var subscribeResult = await socketConnection.TrySubscribeAsync(subscription, true, ct).ConfigureAwait(false);
if (!subscribeResult)
return new CallResult<UpdateSubscription>(subscribeResult.Error!);
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
}
/// <summary>
@@ -335,14 +352,14 @@ namespace CryptoExchange.Net.Clients
/// <param name="connectionFactory">The factory for creating a socket connection</param>
/// <param name="ct">Cancellation token for closing this subscription</param>
/// <returns></returns>
protected virtual async Task<CallResult<HighPerfUpdateSubscription>> SubscribeHighPerfAsync<TUpdateType>(
protected virtual async Task<WebSocketResult<HighPerfUpdateSubscription>> SubscribeHighPerfAsync<TUpdateType>(
string url,
HighPerfSubscription<TUpdateType> subscription,
IHighPerfConnectionFactory connectionFactory,
CancellationToken ct)
{
if (_disposing)
return new CallResult<HighPerfUpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
if (_disposed)
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, new InvalidOperationError("Client disposed, can't subscribe"));
HighPerfSocketConnection<TUpdateType> socketConnection;
var released = false;
@@ -354,7 +371,7 @@ namespace CryptoExchange.Net.Clients
}
catch (OperationCanceledException tce)
{
return new CallResult<HighPerfUpdateSubscription>(new CancellationRequestedError(tce));
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, new CancellationRequestedError(tce));
}
try
@@ -363,8 +380,8 @@ namespace CryptoExchange.Net.Clients
{
// Get a new or existing socket connection
var socketResult = await GetHighPerfSocketConnection<TUpdateType>(url, connectionFactory, ct).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<HighPerfUpdateSubscription>(null);
if (!socketResult.Success)
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, socketResult.Error);
socketConnection = socketResult.Data;
@@ -384,8 +401,8 @@ namespace CryptoExchange.Net.Clients
}
var connectResult = await ConnectIfNeededAsync(socketConnection, false, ct).ConfigureAwait(false);
if (!connectResult)
return new CallResult<HighPerfUpdateSubscription>(connectResult.Error!);
if (!connectResult.Success)
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, connectResult.Error!);
break;
}
@@ -401,10 +418,10 @@ namespace CryptoExchange.Net.Clients
{
// Send the request and wait for answer
var sendResult = await socketConnection.SendAsync(subRequest).ConfigureAwait(false);
if (!sendResult)
if (!sendResult.Success)
{
await socketConnection.CloseAsync().ConfigureAwait(false);
return new CallResult<HighPerfUpdateSubscription>(sendResult.Error!);
return WebSocketResult.Fail<HighPerfUpdateSubscription>(Exchange, sendResult.Error!);
}
}
@@ -418,7 +435,13 @@ namespace CryptoExchange.Net.Clients
}
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
return new CallResult<HighPerfUpdateSubscription>(new HighPerfUpdateSubscription(socketConnection, subscription));
return WebSocketResult.Ok(
Exchange,
socketConnection.SocketId,
default,
default,
socketConnection.ConnectionUri.ToString(),
new HighPerfUpdateSubscription(socketConnection, subscription));
}
/// <summary>
@@ -428,7 +451,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="query">The query</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<THandlerResponse>(Query<THandlerResponse> query, CancellationToken ct = default)
protected virtual Task<QueryResult<THandlerResponse>> QueryAsync<THandlerResponse>(Query<THandlerResponse> query, CancellationToken ct = default)
{
return QueryAsync(BaseAddress, query, ct);
}
@@ -441,13 +464,13 @@ namespace CryptoExchange.Net.Clients
/// <param name="query">The query</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<THandlerResponse>(string url, Query<THandlerResponse> query, CancellationToken ct = default)
protected virtual async Task<QueryResult<THandlerResponse>> QueryAsync<THandlerResponse>(string url, Query<THandlerResponse> query, CancellationToken ct = default)
{
if (_disposing)
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
if (_disposed)
return QueryResult.Fail<THandlerResponse>(Exchange, new InvalidOperationError("Client disposed, can't query"));
if (ct.IsCancellationRequested)
return new CallResult<THandlerResponse>(new CancellationRequestedError());
return QueryResult.Fail<THandlerResponse>(Exchange, new CancellationRequestedError());
SocketConnection socketConnection;
var released = false;
@@ -455,8 +478,8 @@ namespace CryptoExchange.Net.Clients
try
{
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<THandlerResponse>(default);
if (!socketResult.Success)
return QueryResult.Fail<THandlerResponse>(Exchange, socketResult.Error);
socketConnection = socketResult.Data;
@@ -468,8 +491,8 @@ namespace CryptoExchange.Net.Clients
}
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false);
if (!connectResult)
return new CallResult<THandlerResponse>(connectResult.Error!);
if (!connectResult.Success)
return QueryResult.Fail<THandlerResponse>(Exchange, connectResult.Error!);
}
finally
{
@@ -480,11 +503,11 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity)
{
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
return new CallResult<THandlerResponse>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
return QueryResult.Fail<THandlerResponse>(Exchange, new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
}
if (ct.IsCancellationRequested)
return new CallResult<THandlerResponse>(new CancellationRequestedError());
return QueryResult.Fail<THandlerResponse>(Exchange, new CancellationRequestedError());
return await socketConnection.SendAndWaitQueryAsync(query, ct).ConfigureAwait(false);
}
@@ -499,23 +522,23 @@ namespace CryptoExchange.Net.Clients
protected virtual async Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct)
{
if (socket.Connected)
return CallResult.SuccessResult;
return CallResult.Ok();
var connectResult = await ConnectSocketAsync(socket, ct).ConfigureAwait(false);
if (!connectResult)
if (!connectResult.Success)
return connectResult;
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
if (!authenticated || socket.Authenticated)
return CallResult.SuccessResult;
return CallResult.Ok();
if (socket is not SocketConnection sc)
throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication");
var result = await AuthenticateSocketAsync(sc).ConfigureAwait(false);
if (!result)
if (!result.Success)
await socket.CloseAsync().ConfigureAwait(false);
return result;
@@ -529,29 +552,28 @@ namespace CryptoExchange.Net.Clients
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
{
if (GetAuthenticationProvider() == null)
return new CallResult(new NoApiCredentialsError());
return CallResult.Fail(new NoApiCredentialsError());
_logger.AttemptingToAuthenticate(socket.SocketId);
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false);
if (authRequest != null)
{
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
if (!result)
if (!result.Success)
{
_logger.AuthenticationFailed(socket.SocketId);
if (socket.Connected)
await socket.CloseAsync().ConfigureAwait(false);
result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult(result.Error)!;
return CallResult.Fail(result.Error)!;
}
_logger.Authenticated(socket.SocketId);
}
socket.Authenticated = true;
return CallResult.SuccessResult;
return CallResult.Ok();
}
/// <summary>
@@ -580,7 +602,7 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
protected virtual Task<CallResult<string?>> GetConnectionUrlAsync(string address, bool authentication)
{
return Task.FromResult(new CallResult<string?>(address));
return Task.FromResult(CallResult.Ok<string?>(address));
}
/// <summary>
@@ -600,7 +622,7 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns>
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
{
return Task.FromResult(CallResult.SuccessResult);
return Task.FromResult(CallResult.Ok());
}
/// <summary>
@@ -621,24 +643,23 @@ namespace CryptoExchange.Net.Clients
string? topic = null,
int individualSubscriptionCount = 1)
{
var socketQuery = _socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.ApiClient.GetType() == GetType()
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)))
.Select(x => x.Value)
.ToList();
var socketQuery = _socketConnections.Where(s => s.Value.ConnectionUriString.Equals(address.TrimEnd('/'), StringComparison.Ordinal)
&& s.Value.ApiClient.ClientName.Equals(ClientName, StringComparison.Ordinal)
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)))
.Select(x => x.Value); // Don't ToList this so the query is executed again when called
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
var delayStart = DateTime.UtcNow;
var delayed = false;
while (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
while (socketQuery.Count() >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
{
if (DateTime.UtcNow - delayStart > TimeSpan.FromSeconds(10))
{
if (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
if (socketQuery.Count() >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
{
// If after this time we still trying to reconnect/reprocess there is some issue in the connection
_logger.TimeoutWaitingForReconnectingSocket();
return new CallResult<SocketConnection>(new CantConnectError());
return CallResult.Fail<SocketConnection>(new CantConnectError());
}
break;
@@ -648,7 +669,7 @@ namespace CryptoExchange.Net.Clients
try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { }
if (ct.IsCancellationRequested)
return new CallResult<SocketConnection>(new CancellationRequestedError());
return CallResult.Fail<SocketConnection>(new CancellationRequestedError());
}
if (delayed)
@@ -661,7 +682,10 @@ namespace CryptoExchange.Net.Clients
SocketConnection? connection;
if (!dedicatedRequestConnection)
{
connection = socketQuery.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
connection = socketQuery
.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection)
.OrderBy(s => s.UserSubscriptionCount)
.FirstOrDefault();
}
else
{
@@ -687,29 +711,29 @@ namespace CryptoExchange.Net.Clients
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
// If there is a max subscriptions per connection limit also only use existing if the new subscription doesn't go over the limit
if (MaxIndividualSubscriptionsPerConnection == null)
return new CallResult<SocketConnection>(connection);
return CallResult.Ok(connection);
var currentCount = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
if (currentCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection)
return new CallResult<SocketConnection>(connection);
return CallResult.Ok(connection);
}
}
if (maxConnectionsReached)
return new CallResult<SocketConnection>(new InvalidOperationError("Max amount of socket connections reached"));
return CallResult.Fail<SocketConnection>(new InvalidOperationError("Max amount of socket connections reached"));
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
if (!connectionAddress)
if (!connectionAddress.Success)
{
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString());
return connectionAddress.As<SocketConnection>(null);
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error.ToString());
return CallResult.Fail<SocketConnection>(connectionAddress.Error);
}
if (connectionAddress.Data != address)
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
// Create new socket connection
var socketConnection = new SocketConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address);
var socketConnection = new SocketConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this);
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
if (dedicatedRequestConnection)
{
@@ -726,7 +750,7 @@ namespace CryptoExchange.Net.Clients
foreach (var systemSubscription in systemSubscriptions)
socketConnection.AddSubscription(systemSubscription);
return new CallResult<SocketConnection>(socketConnection);
return CallResult.Ok(socketConnection);
}
@@ -743,21 +767,21 @@ namespace CryptoExchange.Net.Clients
CancellationToken ct)
{
var connectionAddress = await GetConnectionUrlAsync(address, false).ConfigureAwait(false);
if (!connectionAddress)
if (!connectionAddress.Success)
{
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString());
return connectionAddress.As<HighPerfSocketConnection<TUpdateType>>(null);
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error.ToString());
return CallResult.Fail<HighPerfSocketConnection<TUpdateType>>(connectionAddress.Error);
}
if (connectionAddress.Data != address)
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
// Create new socket connection
var socketConnection = connectionFactory.CreateHighPerfConnection<TUpdateType>(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address);
var socketConnection = connectionFactory.CreateHighPerfConnection<TUpdateType>(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this);
foreach (var ptg in PeriodicTaskRegistrations)
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, (con) => ptg.QueryDelegate(con).Request);
return new CallResult<HighPerfSocketConnection<TUpdateType>>(socketConnection);
return CallResult.Ok(socketConnection);
}
/// <summary>
@@ -791,7 +815,7 @@ namespace CryptoExchange.Net.Clients
protected virtual async Task<CallResult> ConnectSocketAsync(ISocketConnection socketConnection, CancellationToken ct)
{
var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false);
if (connectResult)
if (connectResult.Success)
{
if (socketConnection is SocketConnection sc)
_socketConnections.TryAdd(socketConnection.SocketId, sc);
@@ -875,7 +899,7 @@ namespace CryptoExchange.Net.Clients
_logger.UnsubscribingAll(sum);
var tasks = new List<Task>();
var socketList = _socketConnections.Values;
foreach (var connection in socketList)
{
@@ -914,15 +938,15 @@ namespace CryptoExchange.Net.Clients
foreach (var item in DedicatedConnectionConfigs)
{
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false);
if (!socketResult)
return socketResult.AsDataless();
if (!socketResult.Success)
return CallResult.Fail(socketResult.Error);
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated, default).ConfigureAwait(false);
if (!connectResult)
return new CallResult(connectResult.Error!);
if (!connectResult.Success)
return CallResult.Fail(connectResult.Error!);
}
return CallResult.SuccessResult;
return CallResult.Ok();
}
/// <summary>
@@ -1004,23 +1028,28 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Dispose the client
/// </summary>
public override void Dispose()
protected override void Dispose(bool disposing)
{
_disposing = true;
var tasks = new List<Task>();
if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
{
var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
if (socketList.Any())
_logger.DisposingSocketClient();
if (!disposing)
return;
foreach (var connection in socketList)
var tasks = new List<Task>();
{
tasks.Add(connection.CloseAsync());
}
}
var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
if (socketList.Any())
_logger.DisposingSocketClient();
semaphoreSlim?.Dispose();
base.Dispose();
foreach (var connection in socketList)
{
tasks.Add(connection.CloseAsync());
}
}
semaphoreSlim?.Dispose();
base.Dispose(disposing);
}
}
/// <summary>
@@ -1071,11 +1100,13 @@ namespace CryptoExchange.Net.Clients
/// ctor
/// </summary>
protected SocketApiClient(
ILogger logger,
ILoggerFactory? loggerFactory,
string exchangeName,
string baseAddress,
SocketExchangeOptions<TEnvironment> options,
SocketApiOptions apiOptions) : base(
logger,
loggerFactory,
exchangeName,
baseAddress,
options,
apiOptions)
@@ -1101,11 +1132,13 @@ namespace CryptoExchange.Net.Clients
/// ctor
/// </summary>
protected SocketApiClient(
ILogger logger,
ILoggerFactory? loggerFactory,
string exchangeName,
string baseAddress,
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
SocketApiOptions apiOptions) : base(
logger,
loggerFactory,
exchangeName,
baseAddress,
options,
apiOptions)
@@ -1132,13 +1165,18 @@ namespace CryptoExchange.Net.Clients
where TApiCredentials : ApiCredentials
where TEnvironment : TradeEnvironment
{
private bool _authProviderInitialized = false;
private TAuthenticationProvider? _authenticationProvider;
/// <summary>
/// Auth provider initialized field
/// </summary>
protected bool _authProviderInitialized = false;
/// <summary>
/// Auth provider field
/// </summary>
protected TAuthenticationProvider? _authenticationProvider;
/// <summary>
/// The authentication provider for this API client. (null if no credentials are set)
/// </summary>
public TAuthenticationProvider? AuthenticationProvider
public virtual TAuthenticationProvider? AuthenticationProvider
{
get
{
@@ -1152,7 +1190,7 @@ namespace CryptoExchange.Net.Clients
return _authenticationProvider;
}
internal set => _authenticationProvider = value;
protected internal set => _authenticationProvider = value;
}
/// <inheritdoc />
@@ -1162,11 +1200,13 @@ namespace CryptoExchange.Net.Clients
/// ctor
/// </summary>
protected SocketApiClient(
ILogger logger,
ILoggerFactory? loggerFactory,
string exchangeName,
string baseAddress,
SocketExchangeOptions<TEnvironment, TApiCredentials> options,
SocketApiOptions apiOptions) : base(
logger,
loggerFactory,
exchangeName,
baseAddress,
options,
apiOptions)
@@ -0,0 +1,172 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
namespace CryptoExchange.Net.Clients
{
/// <inheritdoc />
public abstract class UserClientProvider<TRestClient, TSocketClient, TRestOptions, TSocketOptions, TCredentials, TEnvironment>
where TRestClient : IRestClient<TCredentials>
where TSocketClient : ISocketClient<TCredentials>
where TRestOptions : RestExchangeOptions<TEnvironment, TCredentials>, new()
where TSocketOptions : SocketExchangeOptions<TEnvironment, TCredentials>, new()
where TCredentials : ApiCredentials
where TEnvironment : TradeEnvironment
{
private ConcurrentDictionary<string, TRestClient> _restClients = new ConcurrentDictionary<string, TRestClient>();
private ConcurrentDictionary<string, TSocketClient> _socketClients = new ConcurrentDictionary<string, TSocketClient>();
private readonly IOptions<TRestOptions> _restOptions;
private readonly IOptions<TSocketOptions> _socketOptions;
private readonly HttpClient _httpClient;
private readonly ILoggerFactory? _loggerFactory;
/// <inheritdoc />
public abstract string ExchangeName { get; }
/// <summary>
/// ctor
/// </summary>
public UserClientProvider(
HttpClient? httpClient,
ILoggerFactory? loggerFactory,
IOptions<TRestOptions> restOptions,
IOptions<TSocketOptions> socketOptions)
{
_httpClient = httpClient ?? new HttpClient();
_httpClient.Timeout = restOptions.Value.RequestTimeout;
_loggerFactory = loggerFactory;
_restOptions = restOptions;
_socketOptions = socketOptions;
}
private IOptions<TRestOptions> SetRestEnvironment(IOptions<TRestOptions> options, TEnvironment? environment)
{
if (environment == null)
return options;
var newRestClientOptions = new TRestOptions();
options.Value.Set(newRestClientOptions);
newRestClientOptions.Environment = environment;
return Options.Create(newRestClientOptions);
}
private IOptions<TSocketOptions> SetSocketEnvironment(IOptions<TSocketOptions> options, TEnvironment? environment)
{
if (environment == null)
return options;
var newSocketClientOptions = new TSocketOptions();
options.Value.Set(newSocketClientOptions);
newSocketClientOptions.Environment = environment;
return Options.Create(newSocketClientOptions);
}
/// <inheritdoc />
public void InitializeUserClient(string userIdentifier, TCredentials credentials, TEnvironment? environment = null)
{
CreateRestClient(userIdentifier, credentials, environment);
CreateSocketClient(userIdentifier, credentials, environment);
}
/// <inheritdoc />
public TRestClient GetRestClient(string userIdentifier, TCredentials? credentials = null, TEnvironment? environment = null)
{
if (!_restClients.TryGetValue(userIdentifier, out var client) || client.Disposed)
client = CreateRestClient(userIdentifier, credentials, environment);
return client;
}
/// <inheritdoc />
public TSocketClient GetSocketClient(string userIdentifier, TCredentials? credentials = null, TEnvironment? environment = null)
{
if (!_socketClients.TryGetValue(userIdentifier, out var client) || client.Disposed)
client = CreateSocketClient(userIdentifier, credentials, environment);
return client;
}
private TRestClient CreateRestClient(string userIdentifier, TCredentials? credentials, TEnvironment? environment)
{
var clientRestOptions = SetRestEnvironment(_restOptions, environment);
var client = ConstructRestClient(_httpClient, _loggerFactory, clientRestOptions);
if (credentials != null)
{
_restClients[userIdentifier] = client;
client.SetApiCredentials(credentials);
}
return client;
}
private TSocketClient CreateSocketClient(string userIdentifier, TCredentials? credentials, TEnvironment? environment)
{
var clientSocketOptions = SetSocketEnvironment(_socketOptions, environment);
var client = ConstructSocketClient(_loggerFactory, clientSocketOptions);
if (credentials != null)
{
_socketClients[userIdentifier] = client;
client.SetApiCredentials(credentials);
}
return client;
}
/// <summary>
/// Constructs a new instance of the rest client
/// </summary>
protected abstract TRestClient ConstructRestClient(
HttpClient client,
ILoggerFactory? loggerFactory,
IOptions<TRestOptions> options);
/// <summary>
/// Constructs a new instance of the socket client
/// </summary>
protected abstract TSocketClient ConstructSocketClient(
ILoggerFactory? loggerFactory,
IOptions<TSocketOptions> options);
/// <inheritdoc />
public void ClearUserClients(string userIdentifier)
{
_restClients.TryRemove(userIdentifier, out var restClient);
_socketClients.TryRemove(userIdentifier, out var socketClient);
restClient?.Dispose();
socketClient?.Dispose();
}
/// <inheritdoc />
public void Clear()
{
foreach (var client in _restClients.Values)
client.Dispose();
_restClients.Clear();
foreach (var client in _socketClients.Values)
client.Dispose();
_socketClients.Clear();
}
/// <summary>
/// Applies the provided options delegate to a new instance of the specified type.
/// </summary>
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T : new()
{
var opts = new T();
del?.Invoke(opts);
return opts;
}
}
}
@@ -56,14 +56,26 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.False)
return false;
var value = reader.TokenType switch
if (reader.TokenType == JsonTokenType.Number)
{
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetInt16().ToString(),
_ => null
};
var number = reader.GetInt16();
if (number > 1)
return true;
value = value?.ToLowerInvariant().Trim();
return false;
}
if (reader.TokenType == JsonTokenType.Null)
{
if (typeToConvert == typeof(bool))
LibraryHelpers.StaticLogger?.LogWarning("Received null bool value, but property type is not a nullable bool. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
return default;
}
if (reader.TokenType != JsonTokenType.String)
throw new SerializationException($"Can't convert bool value for token type {reader.TokenType}");
var value = reader.GetString()?.ToLowerInvariant().Trim();
if (string.IsNullOrEmpty(value))
{
if (typeToConvert == typeof(bool))
@@ -73,12 +85,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
switch (value)
{
case "enabled":
case "true":
case "yes":
case "y":
case "1":
case "on":
return true;
case "disabled":
case "false":
case "no":
case "n":
@@ -88,7 +102,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return false;
}
throw new SerializationException($"Can't convert bool value {value}");
throw new SerializationException($"Can't convert bool value, unknown string value: {value}");
}
}
@@ -16,17 +16,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
private static Type _dateTimeType = typeof(DateTime);
private static Type _nullableDateTimeType = typeof(DateTime?);
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
return typeToConvert == _dateTimeType || typeToConvert == _nullableDateTimeType;
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new NullableDateTimeConverterInner();
return typeToConvert == _dateTimeType ? new DateTimeConverterInner() : new NullableDateTimeConverterInner();
}
private class NullableDateTimeConverterInner : JsonConverter<DateTime?>
@@ -68,7 +70,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
if (reader.TokenType == JsonTokenType.Null)
{
if (typeToConvert == typeof(DateTime))
if (typeToConvert == _dateTimeType)
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
return default;
}
@@ -76,7 +78,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType is JsonTokenType.Number)
{
var decValue = reader.GetDecimal();
if (decValue == 0 || decValue < 0)
if (decValue <= 0)
return default;
return ParseFromDecimal(decValue);
@@ -86,8 +88,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var stringValue = reader.GetString();
if (string.IsNullOrWhiteSpace(stringValue)
|| stringValue!.Equals("-1", StringComparison.Ordinal)
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase)
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase))
{
return default;
}
@@ -124,7 +125,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <summary>
/// Parse a string value to datetime
/// </summary>
public static DateTime ParseFromString(string stringValue, string? resolverName)
public static DateTime? ParseFromString(string stringValue, string? resolverName)
{
if (stringValue!.Length == 12 && stringValue.StartsWith("202", StringComparison.OrdinalIgnoreCase))
{
+3 -3
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>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.</Description>
<PackageVersion>11.2.2</PackageVersion>
<AssemblyVersion>11.2.2</AssemblyVersion>
<FileVersion>11.2.2</FileVersion>
<PackageVersion>12.0.0-beta1</PackageVersion>
<AssemblyVersion>12.0.0</AssemblyVersion>
<FileVersion>12.0.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType>
+9 -9
View File
@@ -311,16 +311,16 @@ namespace CryptoExchange.Net
/// <param name="request">The request parameters</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, PageRequest?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
public static async IAsyncEnumerable<HttpResult<T[]>> ExecutePages<T, U>(Func<U, PageRequest?, CancellationToken, Task<HttpResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
{
var result = new List<T>();
ExchangeWebResult<T[]> batch;
HttpResult<T[]> batch;
PageRequest? nextPageToken = null;
while (true)
{
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
yield return batch;
if (!batch || ct.IsCancellationRequested)
if (!batch.Success || ct.IsCancellationRequested)
break;
result.AddRange(batch.Data);
@@ -399,8 +399,8 @@ namespace CryptoExchange.Net
/// <param name="asyncHandler">The async update handler</param>
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
public static async Task<CallResult<UpdateSubscription>> ProcessQueuedAsync<T>(
Func<Action<DataEvent<T>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
public static async Task<WebSocketResult<UpdateSubscription>> ProcessQueuedAsync<T>(
Func<Action<DataEvent<T>>, Task<WebSocketResult<UpdateSubscription>>> subscribeCall,
Func<DataEvent<T>, Task> asyncHandler,
int? maxQueuedItems = null,
QueueFullBehavior? fullBehavior = null)
@@ -408,7 +408,7 @@ namespace CryptoExchange.Net
var processor = new ProcessQueue<DataEvent<T>>(asyncHandler, maxQueuedItems, fullBehavior);
await processor.StartAsync().ConfigureAwait(false);
var result = await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
if (!result)
if (!result.Success)
{
await processor.StopAsync().ConfigureAwait(false);
return result;
@@ -473,7 +473,7 @@ namespace CryptoExchange.Net
}, maxQueuedItems, fullBehavior);
await processor.StartAsync().ConfigureAwait(false);
var result = await subscribeCall(processor).ConfigureAwait(false);
if (!result)
if (!result.Success)
{
await processor.StopAsync().ConfigureAwait(false);
return result;
@@ -499,7 +499,7 @@ namespace CryptoExchange.Net
return null;
// Try parse, only fails for these reasons:
// 1. string is null or empty
// 1. string is null or empty (already covered)
// 2. value is larger or smaller than decimal max/min
// 3. unparsable format
if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue))
@@ -516,7 +516,7 @@ namespace CryptoExchange.Net
if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase))
return decimal.MaxValue;
else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase))
return decimal.MinValue;
return decimal.MinValue;
if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue))
{
+215 -37
View File
@@ -11,85 +11,92 @@ namespace CryptoExchange.Net
/// </summary>
public static class ExchangeSymbolCache
{
private static ConcurrentDictionary<string, ExchangeInfo> _symbolInfos = new ConcurrentDictionary<string, ExchangeInfo>();
private static ConcurrentDictionary<string, ExchangeKeyedCache> _symbolInfos = new ConcurrentDictionary<string, ExchangeKeyedCache>();
/// <summary>
/// Update the cached symbol data for an exchange
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="environment">Trading environment</param>
/// <param name="key">Optional data set key</param>
/// <param name="updateData">Symbol data</param>
public static void UpdateSymbolInfo(string topicId, SharedSpotSymbol[] updateData)
public static void UpdateSymbolInfo(string topicId, string environment, string? key, SharedSpotSymbol[] updateData)
{
if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
var id = topicId + environment;
if(!_symbolInfos.TryGetValue(id, out var exchangeInfo))
{
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
_symbolInfos.TryAdd(topicId, exchangeInfo);
exchangeInfo = new ExchangeKeyedCache();
_symbolInfos.TryAdd(id, exchangeInfo);
}
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
var keyedCache = exchangeInfo.Get(key);
if (keyedCache != null && DateTime.UtcNow - keyedCache.UpdateTime < TimeSpan.FromMinutes(60))
return;
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
exchangeInfo.Set(key, new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol)));
}
/// <summary>
/// Whether the specific topic has been cached
/// </summary>
/// <param name="topicId">Id</param>
public static bool HasCached(string topicId)
/// <param name="environment">Trading environment</param>
/// <param name="key">Optional data set key</param>
public static bool HasCached(string topicId, string environment, string? key)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
var id = topicId + environment;
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
return false;
return exchangeInfo.Symbols.Count > 0;
return exchangeInfo.HasCached(key);
}
/// <summary>
/// Whether a specific exchange(topic) support the provided symbol
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="environment">Trading environment</param>
/// <param name="key">Optional data set key</param>
/// <param name="symbolName">The symbol name</param>
public static bool SupportsSymbol(string topicId, string symbolName)
public static bool SupportsSymbol(string topicId, string environment, string? key, string symbolName)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
var id = topicId + environment;
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
return false;
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
return false;
return true;
return exchangeInfo.SupportsSymbol(key, symbolName);
}
/// <summary>
/// Whether a specific exchange(topic) support the provided symbol
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="environment">Trading environment</param>
/// <param name="key">Optional data set key</param>
/// <param name="symbol">The symbol info</param>
public static bool SupportsSymbol(string topicId, SharedSymbol symbol)
public static bool SupportsSymbol(string topicId, string environment, string? key, SharedSymbol symbol)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
var id = topicId + environment;
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
return false;
return exchangeInfo.Symbols.Any(x =>
x.Value.TradingMode == symbol.TradingMode
&& x.Value.BaseAsset == symbol.BaseAsset
&& x.Value.QuoteAsset == symbol.QuoteAsset);
return exchangeInfo.SupportsSymbol(key, symbol);
}
/// <summary>
/// Get all symbols for a specific base asset
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="environment">Trading environment</param>
/// <param name="key">Optional data set key</param>
/// <param name="baseAsset">Base asset name</param>
public static SharedSymbol[] GetSymbolsForBaseAsset(string topicId, string baseAsset)
public static SharedSymbol[] GetSymbolsForBaseAsset(string topicId, string environment, string? key, string baseAsset)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
var id = topicId + environment;
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
return [];
return exchangeInfo.Symbols
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
.Select(x => x.Value)
.ToArray();
return exchangeInfo.GetSymbolsForBaseAsset(key, baseAsset);
}
/// <summary>
@@ -97,23 +104,194 @@ namespace CryptoExchange.Net
/// </summary>
/// <param name="topicId">Id for the provided data</param>
/// <param name="symbolName">Symbol name</param>
public static SharedSymbol? ParseSymbol(string topicId, string? symbolName)
/// <param name="environment">Trade environment</param>
/// <param name="key">Additional data set identification key</param>
public static SharedSymbol? ParseSymbol(string topicId, string environment, string? key, string? symbolName)
{
if (symbolName == null)
return null;
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
var id = topicId + environment;
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
return null;
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
return null;
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
{
DeliverTime = symbolInfo.DeliverTime
};
return exchangeInfo.ParseSymbol(key, symbolName);
}
class ExchangeKeyedCache
{
private ExchangeInfo? _noKeyCache;
private ConcurrentDictionary<string, ExchangeInfo> _keyedCache = new ConcurrentDictionary<string, ExchangeInfo>();
public ExchangeInfo? Get(string? key)
{
if (key == null)
return _noKeyCache;
if (_keyedCache.TryGetValue(key, out var exchangeInfo))
return exchangeInfo;
return null;
}
public void Set(string? key, ExchangeInfo exchangeInfo)
{
if (key == null)
_noKeyCache = exchangeInfo;
else
_keyedCache[key] = exchangeInfo;
}
public bool HasCached(string? key)
{
if (key == null)
{
if (_noKeyCache?.Symbols.Count > 0)
return true;
foreach (var cache in _keyedCache.Values)
{
if (cache.Symbols.Count > 0)
return true;
}
return false;
}
return _keyedCache.TryGetValue(key, out var exchangeInfo) && exchangeInfo.Symbols.Count > 0;
}
public SharedSymbol? ParseSymbol(string? key, string symbolName)
{
SharedSymbol? symbolInfo = null;
if (key == null)
{
if (_noKeyCache != null)
{
if (!_noKeyCache.Symbols.TryGetValue(symbolName, out symbolInfo))
return null;
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
{
DeliverTime = symbolInfo.DeliverTime
};
}
foreach(var cache in _keyedCache.Values)
{
if (cache.Symbols.TryGetValue(symbolName, out symbolInfo))
{
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
{
DeliverTime = symbolInfo.DeliverTime
};
}
}
return null;
}
var hasKeyedSet = _keyedCache.TryGetValue(key, out var exchangeInfo);
if (!hasKeyedSet || exchangeInfo == null)
return null;
if (exchangeInfo.Symbols.TryGetValue(symbolName, out symbolInfo))
{
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
{
DeliverTime = symbolInfo.DeliverTime
};
}
return null;
}
public bool SupportsSymbol(string? key, string symbolName)
{
if (key == null)
{
if (_noKeyCache?.Symbols.ContainsKey(symbolName) == true)
return true;
foreach(var cache in _keyedCache.Values)
{
if (cache.Symbols.ContainsKey(symbolName))
return true;
}
return false;
}
return _keyedCache.TryGetValue(key, out var exchangeInfo) && exchangeInfo.Symbols.ContainsKey(symbolName);
}
public bool SupportsSymbol(string? key, SharedSymbol symbol)
{
if (key == null)
{
if (_noKeyCache?.Symbols.Any(x =>
x.Value.TradingMode == symbol.TradingMode
&& x.Value.BaseAsset == symbol.BaseAsset
&& x.Value.QuoteAsset == symbol.QuoteAsset) == true)
{
return true;
}
foreach (var cache in _keyedCache.Values)
{
if (cache.Symbols.Any(x =>
x.Value.TradingMode == symbol.TradingMode
&& x.Value.BaseAsset == symbol.BaseAsset
&& x.Value.QuoteAsset == symbol.QuoteAsset))
{
return true;
}
}
return false;
}
return _keyedCache.TryGetValue(key, out var exchangeInfo) && exchangeInfo.Symbols.Any(x =>
x.Value.TradingMode == symbol.TradingMode
&& x.Value.BaseAsset == symbol.BaseAsset
&& x.Value.QuoteAsset == symbol.QuoteAsset);
}
public SharedSymbol[] GetSymbolsForBaseAsset(string? key, string baseAsset)
{
if (key == null)
{
if (_noKeyCache != null)
{
return _noKeyCache.Symbols
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
.Select(x => x.Value)
.ToArray();
}
var result = new List<SharedSymbol>();
foreach(var cache in _keyedCache.Values)
{
result.AddRange(cache.Symbols
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
.Select(x => x.Value));
}
return result.ToArray();
}
var hasKeyedSet = _keyedCache.TryGetValue(key, out var exchangeInfo);
if (!hasKeyedSet || exchangeInfo == null)
return [];
return exchangeInfo.Symbols
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
.Select(x => x.Value)
.ToArray();
}
}
class ExchangeInfo
{
public DateTime UpdateTime { get; set; }
+5 -6
View File
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.DependencyInjection;
using System;
@@ -24,7 +25,7 @@ namespace CryptoExchange.Net
/// <param name="parameters"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static void AddParameter(this Dictionary<string, object> parameters, string key, string value)
public static void AddParameter(this IDictionary<string, object> parameters, string key, string value)
{
parameters.Add(key, value);
}
@@ -35,7 +36,7 @@ namespace CryptoExchange.Net
/// <param name="parameters"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static void AddParameter(this Dictionary<string, object> parameters, string key, object value)
public static void AddParameter(this IDictionary<string, object> parameters, string key, object value)
{
parameters.Add(key, value);
}
@@ -46,7 +47,7 @@ namespace CryptoExchange.Net
/// <param name="parameters"></param>
/// <param name="key"></param>
/// <param name="value"></param>
public static void AddOptionalParameter(this Dictionary<string, object> parameters, string key, object? value)
public static void AddOptionalParameter(this IDictionary<string, object> parameters, string key, object? value)
{
if (value != null)
parameters.Add(key, value);
@@ -378,8 +379,6 @@ namespace CryptoExchange.Net
services.AddTransient(x => (IDepositRestClient)client(x)!);
if (typeof(IKlineRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IKlineRestClient)client(x)!);
if (typeof(IListenKeyRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IListenKeyRestClient)client(x)!);
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
if (typeof(IRecentTradeRestClient).IsAssignableFrom(typeof(T)))
@@ -10,6 +10,10 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// </summary>
public interface IBaseApiClient
{
/// <summary>
/// Exchange name
/// </summary>
string Exchange { get; }
/// <summary>
/// Base address
/// </summary>
@@ -28,6 +28,11 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// </summary>
bool Authenticated { get; }
/// <summary>
/// Configured credentials
/// </summary>
TApiCredentials? ApiCredentials { get; }
/// <summary>
/// Set the API credentials for this API client
/// </summary>
@@ -84,6 +84,12 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
/// </summary>
bool Authenticated { get; }
/// <summary>
/// Configured credentials
/// </summary>
TApiCredentials? ApiCredentials { get; }
/// <summary>
/// Set the API credentials for this API client
/// </summary>
@@ -107,7 +107,7 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
/// <param name="ct">A cancellation token to stop the order book when canceled</param>
/// <returns></returns>
Task<CallResult<bool>> StartAsync(CancellationToken? ct = null);
Task<CallResult> StartAsync(CancellationToken? ct = null);
/// <summary>
/// Stop syncing the order book
@@ -18,32 +18,32 @@ namespace CryptoExchange.Net.Logging.Extensions
_rateLimitRequestFailed = LoggerMessage.Define<int, string, string, string>(
LogLevel.Warning,
new EventId(6000, "RateLimitRequestFailed"),
"[Req {Id}] Call to {Path} failed because of ratelimit guard {Guard}; {Limit}");
"[Req {Id}] call to {Path} failed because of ratelimit guard {Guard}; {Limit}");
_rateLimitConnectionFailed = LoggerMessage.Define<int, string, string>(
LogLevel.Warning,
new EventId(6001, "RateLimitConnectionFailed"),
"[Sckt {Id}] Connection failed because of ratelimit guard {Guard}; {Limit}");
"[Sckt {Id}] connection failed because of ratelimit guard {Guard}; {Limit}");
_rateLimitDelayingRequest = LoggerMessage.Define<int, string, TimeSpan, string, string>(
LogLevel.Warning,
new EventId(6002, "RateLimitDelayingRequest"),
"[Req {Id}] Delaying call to {Path} by {Delay} because of ratelimit guard {Guard}; {Limit}");
"[Req {Id}] delaying call to {Path} by {Delay} because of ratelimit guard {Guard}; {Limit}");
_rateLimitDelayingConnection = LoggerMessage.Define<int, TimeSpan, string, string>(
LogLevel.Warning,
new EventId(6003, "RateLimitDelayingConnection"),
"[Sckt {Id}] Delaying connection by {Delay} because of ratelimit guard {Guard}; {Limit}");
"[Sckt {Id}] delaying connection by {Delay} because of ratelimit guard {Guard}; {Limit}");
_rateLimitAppliedConnection = LoggerMessage.Define<int, string, string, int>(
LogLevel.Trace,
new EventId(6004, "RateLimitDelayingConnection"),
"[Sckt {Id}] Connection passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
"[Sckt {Id}] connection passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
_rateLimitAppliedRequest = LoggerMessage.Define<int, string, string, string, int>(
LogLevel.Trace,
new EventId(6005, "RateLimitAppliedRequest"),
"[Req {Id}] Call to {Path} passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
"[Req {Id}] call to {Path} passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
}
public static void RateLimitRequestFailed(this ILogger logger, int requestId, string path, string guard, string limit)
@@ -28,67 +28,67 @@ namespace CryptoExchange.Net.Logging.Extensions
_restApiErrorReceived = LoggerMessage.Define<int?, int?, long, string?, string?>(
LogLevel.Warning,
new EventId(4000, "RestApiErrorReceived"),
"[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}, Data: {OriginalData}");
"[Req {RequestId}] {ResponseStatusCode} - error received in {ResponseTime}ms: {ErrorMessage}, Data: {OriginalData}");
_restApiResponseReceived = LoggerMessage.Define<int?, int?, long, string?>(
LogLevel.Debug,
new EventId(4001, "RestApiResponseReceived"),
"[Req {RequestId}] {ResponseStatusCode} - Response received in {ResponseTime}ms: {OriginalData}");
"[Req {RequestId}] {ResponseStatusCode} - response received in {ResponseTime}ms: {OriginalData}");
_restApiFailedToSyncTime = LoggerMessage.Define<int, string>(
LogLevel.Debug,
new EventId(4002, "RestApiFailedToSyncTime"),
"[Req {RequestId}] Failed to sync time, aborting request: {ErrorMessage}");
"[Req {RequestId}] failed to sync time, aborting request: {ErrorMessage}");
_restApiNoApiCredentials = LoggerMessage.Define<int, string>(
LogLevel.Warning,
new EventId(4003, "RestApiNoApiCredentials"),
"[Req {RequestId}] Request {RestApiUri} failed because no ApiCredentials were provided");
"[Req {RequestId}] request {RestApiUri} failed because no ApiCredentials were provided");
_restApiCreatingRequest = LoggerMessage.Define<int, Uri>(
LogLevel.Information,
new EventId(4004, "RestApiCreatingRequest"),
"[Req {RequestId}] Creating request for {RestApiUri}");
"[Req {RequestId}] creating request for {RestApiUri}");
_restApiSendingRequest = LoggerMessage.Define<int, HttpMethod, string, Uri, string>(
LogLevel.Trace,
new EventId(4005, "RestApiSendingRequest"),
"[Req {RequestId}] Sending {Method} {Signed} request to {RestApiUri}{Query}");
"[Req {RequestId}] sending {Method} {Signed} request to {RestApiUri}{Query}");
_restApiRateLimitRetry = LoggerMessage.Define<int, DateTime>(
LogLevel.Warning,
new EventId(4006, "RestApiRateLimitRetry"),
"[Req {RequestId}] Received ratelimit error, retrying after {Timestamp}");
"[Req {RequestId}] received ratelimit error, retrying after {Timestamp}");
_restApiRateLimitPauseUntil = LoggerMessage.Define<int, DateTime>(
LogLevel.Warning,
new EventId(4007, "RestApiRateLimitPauseUntil"),
"[Req {RequestId}] Ratelimit error from server, pausing requests until {Until}");
"[Req {RequestId}] ratelimit error from server, pausing requests until {Until}");
_restApiSendRequest = LoggerMessage.Define<int, RequestDefinition, string?, string, string>(
LogLevel.Debug,
new EventId(4008, "RestApiSendRequest"),
"[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
"[Req {RequestId}] sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
_restApiCheckingCache = LoggerMessage.Define<string>(
LogLevel.Trace,
new EventId(4009, "RestApiCheckingCache"),
"Checking cache for key {Key}");
"checking cache for key {Key}");
_restApiCacheHit = LoggerMessage.Define<string>(
LogLevel.Trace,
new EventId(4010, "RestApiCacheHit"),
"Cache hit for key {Key}");
"cache hit for key {Key}");
_restApiCacheNotHit = LoggerMessage.Define<string>(
LogLevel.Trace,
new EventId(4011, "RestApiCacheNotHit"),
"Cache not hit for key {Key}");
"cache not hit for key {Key}");
_restApiCancellationRequested = LoggerMessage.Define<int?>(
LogLevel.Debug,
new EventId(4012, "RestApiCancellationRequested"),
"[Req {RequestId}] Request cancelled by user");
"[Req {RequestId}] request cancelled by user");
}
@@ -61,7 +61,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_attemptingToAuthenticate = LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(3006, "AttemptingToAuthenticate"),
"[Sckt {SocketId}] Attempting to authenticate");
"[Sckt {SocketId}] attempting to authenticate");
_authenticationFailed = LoggerMessage.Define<int>(
LogLevel.Warning,
@@ -76,12 +76,12 @@ namespace CryptoExchange.Net.Logging.Extensions
_failedToDetermineConnectionUrl = LoggerMessage.Define<string?>(
LogLevel.Warning,
new EventId(3009, "FailedToDetermineConnectionUrl"),
"Failed to determine connection url: {ErrorMessage}");
"failed to determine connection url: {ErrorMessage}");
_connectionAddressSetTo = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(3010, "ConnectionAddressSetTo"),
"Connection address set to {ConnectionAddress}");
"connection address set to {ConnectionAddress}");
_socketCreatedForAddress = LoggerMessage.Define<int, string>(
LogLevel.Debug,
@@ -91,37 +91,37 @@ namespace CryptoExchange.Net.Logging.Extensions
_unsubscribingAll = LoggerMessage.Define<int>(
LogLevel.Information,
new EventId(3013, "UnsubscribingAll"),
"Unsubscribing all {SubscriptionCount} subscriptions");
"unsubscribing all {SubscriptionCount} subscriptions");
_disposingSocketClient = LoggerMessage.Define(
LogLevel.Debug,
new EventId(3015, "DisposingSocketClient"),
"Disposing socket client, closing all subscriptions");
"disposing socket client, closing all subscriptions");
_unsubscribingSubscription = LoggerMessage.Define<int, int>(
LogLevel.Information,
new EventId(3016, "UnsubscribingSubscription"),
"[Sckt {SocketId}] Unsubscribing subscription {SubscriptionId}");
"[Sckt {SocketId}] unsubscribing subscription {SubscriptionId}");
_reconnectingAllConnections = LoggerMessage.Define<int>(
LogLevel.Information,
new EventId(3017, "ReconnectingAll"),
"Reconnecting all {ConnectionCount} connections");
"reconnecting all {ConnectionCount} connections");
_addingRetryAfterGuard = LoggerMessage.Define<DateTime>(
LogLevel.Warning,
new EventId(3018, "AddRetryAfterGuard"),
"Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
"adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
_timeoutWaitingForReconnectingSocket = LoggerMessage.Define(
LogLevel.Debug,
new EventId(3019, "TimeoutWaitingForReconnectingSocket"),
"Timeout while waiting for existing socket reconnection, failing request");
"timeout while waiting for existing socket reconnection, failing request");
_waitedForReconnectingSocket = LoggerMessage.Define<long>(
LogLevel.Trace,
new EventId(3020, "WaitedForReconnectingSocket"),
"Waited for reconnecting socket for {Timespan}ms");
"waited for reconnecting socket for {Timespan}ms");
}
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
@@ -60,7 +60,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_unknownExceptionWhileProcessingReconnection = LoggerMessage.Define<int>(
LogLevel.Warning,
new EventId(2003, "UnknownExceptionWhileProcessingReconnection"),
"[Sckt {SocketId}] Unknown exception while processing reconnection, reconnecting again");
"[Sckt {SocketId}] unknown exception while processing reconnection, reconnecting again");
_webSocketErrorCodeAndDetails = LoggerMessage.Define<int, WebSocketError, string?>(
LogLevel.Warning,
-592
View File
@@ -1,592 +0,0 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// The result of an operation
/// </summary>
public class CallResult
{
/// <summary>
/// Static success result
/// </summary>
public static CallResult SuccessResult { get; } = new CallResult(null);
/// <summary>
/// An error if the call didn't succeed, will always be filled if Success = false
/// </summary>
public Error? Error { get; internal set; }
/// <summary>
/// Whether the call was successful
/// </summary>
public bool Success => Error == null;
/// <summary>
/// ctor
/// </summary>
/// <param name="error"></param>
public CallResult(Error? error)
{
Error = error;
}
/// <summary>
/// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success)
/// </summary>
/// <param name="obj"></param>
public static implicit operator bool(CallResult obj)
{
return obj?.Success == true;
}
/// <inheritdoc />
public override string ToString()
{
return Success ? $"Success" : $"Error: {Error}";
}
}
/// <summary>
/// The result of an operation
/// </summary>
/// <typeparam name="T"></typeparam>
public class CallResult<T>: CallResult
{
/// <summary>
/// The data returned by the call, only available when Success = true
/// </summary>
public T Data { get; internal set; }
/// <summary>
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
/// </summary>
public string? OriginalData { get; internal set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="data"></param>
/// <param name="originalData"></param>
/// <param name="error"></param>
#pragma warning disable 8618
public CallResult([AllowNull]T data, string? originalData, Error? error): base(error)
#pragma warning restore 8618
{
OriginalData = originalData;
#pragma warning disable 8601
Data = data;
#pragma warning restore 8601
}
/// <summary>
/// Create a new data result
/// </summary>
/// <param name="data">The data to return</param>
public CallResult(T data) : this(data, null, null) { }
/// <summary>
/// Create a new error result
/// </summary>
/// <param name="error">The error to 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>
/// <param name="obj"></param>
public static implicit operator bool(CallResult<T> obj)
{
return obj?.Success == true;
}
/// <summary>
/// Whether the call was successful or not. Useful for nullability checking.
/// </summary>
/// <param name="data">The data returned by the call.</param>
/// <param name="error"><see cref="Error"/> on failure.</param>
/// <returns><c>true</c> when <see cref="CallResult{T}"/> succeeded, <c>false</c> otherwise.</returns>
public bool GetResultOrError([MaybeNullWhen(false)] out T data, [NotNullWhen(false)] out Error? error)
{
if (Success)
{
data = Data!;
error = null;
return true;
}
else
{
data = default;
error = Error!;
return false;
}
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="data">The data of the new type</param>
/// <returns></returns>
public CallResult<K> As<K>([AllowNull] K data)
{
return new CallResult<K>(data, OriginalData, Error);
}
/// <summary>
/// Copy as a dataless result
/// </summary>
/// <returns></returns>
public CallResult AsDataless()
{
if (Error != null )
return new CallResult(Error);
return SuccessResult;
}
/// <summary>
/// Copy as a dataless result
/// </summary>
/// <returns></returns>
public CallResult AsDatalessError(Error error)
{
return new CallResult(error);
}
/// <summary>
/// Copy the CallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="data">The data</param>
/// <param name="error">The error returned</param>
/// <returns></returns>
public CallResult<K> AsErrorWithData<K>(Error error, K data)
{
return new CallResult<K>(data, OriginalData, error);
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="error">The error to return</param>
/// <returns></returns>
public CallResult<K> AsError<K>(Error error)
{
return new CallResult<K>(default, OriginalData, error);
}
/// <inheritdoc />
public override string ToString()
{
return Success ? $"Success" : $"Error: {Error}";
}
}
/// <summary>
/// The result of a request
/// </summary>
public class WebCallResult : CallResult
{
/// <summary>
/// The request http method
/// </summary>
public HttpMethod? RequestMethod { get; set; }
/// <summary>
/// HTTP protocol version
/// </summary>
public Version? HttpVersion { get; set; }
/// <summary>
/// The headers sent with the request
/// </summary>
public HttpRequestHeaders? RequestHeaders { get; set; }
/// <summary>
/// The request id
/// </summary>
public int? RequestId { get; set; }
/// <summary>
/// The url which was requested
/// </summary>
public string? RequestUrl { get; set; }
/// <summary>
/// The body of the request
/// </summary>
public string? RequestBody { get; set; }
/// <summary>
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
/// </summary>
public string? OriginalData { get; internal set; }
/// <summary>
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
/// </summary>
public HttpStatusCode? ResponseStatusCode { get; set; }
/// <summary>
/// The response headers
/// </summary>
public HttpResponseHeaders? ResponseHeaders { get; set; }
/// <summary>
/// The time between sending the request and receiving the response
/// </summary>
public TimeSpan? ResponseTime { get; set; }
/// <summary>
/// ctor
/// </summary>
public WebCallResult(
HttpStatusCode? code,
Version? httpVersion,
HttpResponseHeaders? responseHeaders,
TimeSpan? responseTime,
string? originalData,
int? requestId,
string? requestUrl,
string? requestBody,
HttpMethod? requestMethod,
HttpRequestHeaders? requestHeaders,
Error? error) : base(error)
{
ResponseStatusCode = code;
HttpVersion = httpVersion;
ResponseHeaders = responseHeaders;
ResponseTime = responseTime;
RequestId = requestId;
OriginalData = originalData;
RequestUrl = requestUrl;
RequestBody = requestBody;
RequestHeaders = requestHeaders;
RequestMethod = requestMethod;
}
/// <summary>
/// ctor
/// </summary>
/// <param name="error"></param>
public WebCallResult(Error error): base(error) { }
/// <summary>
/// Return the result as an error result
/// </summary>
/// <param name="error">The error returned</param>
/// <returns></returns>
public WebCallResult AsError(Error error)
{
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="data">The data of the new type</param>
/// <returns></returns>
public WebCallResult<K> As<K>([AllowNull] K data)
{
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange</param>
/// <param name="tradeMode">Trade mode the result applies to</param>
/// <param name="data">The data</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data)
{
return new ExchangeWebResult<K>(exchange, tradeMode, this.As<K>(data));
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange</param>
/// <param name="tradeModes">Trade modes the result applies to</param>
/// <param name="data">The data</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data)
{
return new ExchangeWebResult<K>(exchange, tradeModes, this.As<K>(data));
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="error">The error returned</param>
/// <returns></returns>
public WebCallResult<K> AsError<K>(Error error)
{
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error);
}
/// <inheritdoc />
public override string ToString()
{
return (Success ? $"Success" : $"Error: {Error}") + $" in {ResponseTime}";
}
}
/// <summary>
/// The result of a request
/// </summary>
/// <typeparam name="T"></typeparam>
public class WebCallResult<T>: CallResult<T>
{
/// <summary>
/// The request http method
/// </summary>
public HttpMethod? RequestMethod { get; set; }
/// <summary>
/// HTTP protocol version
/// </summary>
public Version? HttpVersion { get; set; }
/// <summary>
/// The headers sent with the request
/// </summary>
public HttpRequestHeaders? RequestHeaders { get; set; }
/// <summary>
/// The request id
/// </summary>
public int? RequestId { get; set; }
/// <summary>
/// The url which was requested
/// </summary>
public string? RequestUrl { get; set; }
/// <summary>
/// The body of the request
/// </summary>
public string? RequestBody { get; set; }
/// <summary>
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
/// </summary>
public HttpStatusCode? ResponseStatusCode { get; set; }
/// <summary>
/// Length in bytes of the response
/// </summary>
public long? ResponseLength { get; set; }
/// <summary>
/// The response headers
/// </summary>
public HttpResponseHeaders? ResponseHeaders { get; set; }
/// <summary>
/// The time between sending the request and receiving the response
/// </summary>
public TimeSpan? ResponseTime { get; set; }
/// <summary>
/// The data source of this result
/// </summary>
public ResultDataSource DataSource { get; set; } = ResultDataSource.Server;
/// <summary>
/// Create a new result
/// </summary>
public WebCallResult(
HttpStatusCode? code,
Version? httpVersion,
HttpResponseHeaders? responseHeaders,
TimeSpan? responseTime,
long? responseLength,
string? originalData,
int? requestId,
string? requestUrl,
string? requestBody,
HttpMethod? requestMethod,
HttpRequestHeaders? requestHeaders,
ResultDataSource dataSource,
[AllowNull] T data,
Error? error) : base(data, originalData, error)
{
HttpVersion = httpVersion;
ResponseStatusCode = code;
ResponseHeaders = responseHeaders;
ResponseTime = responseTime;
ResponseLength = responseLength;
RequestId = requestId;
RequestUrl = requestUrl;
RequestBody = requestBody;
RequestHeaders = requestHeaders;
RequestMethod = requestMethod;
DataSource = dataSource;
}
/// <summary>
/// Copy as a dataless result
/// </summary>
/// <returns></returns>
public new WebCallResult AsDataless()
{
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error);
}
/// <summary>
/// Copy as a dataless result
/// </summary>
/// <returns></returns>
public new WebCallResult AsDatalessError(Error error)
{
return new WebCallResult(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
}
/// <summary>
/// Create a new error result
/// </summary>
/// <param name="error">The error</param>
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="data">The data of the new type</param>
/// <returns></returns>
public new WebCallResult<K> As<K>([AllowNull] K data)
{
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="error">The error returned</param>
/// <returns></returns>
public new WebCallResult<K> AsError<K>(Error error)
{
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
}
/// <summary>
/// Copy the WebCallResult to a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="data">The data</param>
/// <param name="error">The error returned</param>
/// <returns></returns>
public new WebCallResult<K> AsErrorWithData<K>(Error error, K data)
{
return new WebCallResult<K>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <param name="exchange">The exchange</param>
/// <param name="tradeMode">Trade mode the result applies to</param>
/// <returns></returns>
public ExchangeWebResult<T> AsExchangeResult(string exchange, TradingMode tradeMode)
{
return new ExchangeWebResult<T>(exchange, tradeMode, this);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <param name="exchange">The exchange</param>
/// <param name="tradeModes">Trade modes the result applies to</param>
/// <returns></returns>
public ExchangeWebResult<T> AsExchangeResult(string exchange, TradingMode[] tradeModes)
{
return new ExchangeWebResult<T>(exchange, tradeModes, this);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange</param>
/// <param name="tradeMode">Trade mode the result applies to</param>
/// <param name="data">Data</param>
/// <param name="nextPageRequest">Next page request</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data, PageRequest? nextPageRequest = null)
{
return new ExchangeWebResult<K>(exchange, tradeMode, As<K>(data), nextPageRequest);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange</param>
/// <param name="tradeModes">Trade modes the result applies to</param>
/// <param name="data">Data</param>
/// <param name="nextPageRequest">Next page token</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, PageRequest? nextPageRequest = null)
{
return new ExchangeWebResult<K>(exchange, tradeModes, As<K>(data), nextPageRequest);
}
/// <summary>
/// Copy the WebCallResult to an ExchangeWebResult with a specific error
/// </summary>
/// <typeparam name="K">The new type</typeparam>
/// <param name="exchange">The exchange</param>
/// <param name="error">The error returned</param>
/// <returns></returns>
public ExchangeWebResult<K> AsExchangeError<K>(string exchange, Error error)
{
return new ExchangeWebResult<K>(exchange, null, AsError<K>(error));
}
/// <summary>
/// Return a copy of this result with data source set to cache
/// </summary>
/// <returns></returns>
internal WebCallResult<T> Cached()
{
return new WebCallResult<T>(ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
}
/// <inheritdoc />
public override string ToString()
{
var sb = new StringBuilder();
sb.Append(Success ? $"Success response" : $"Error response: {Error}");
if (ResponseLength != null)
sb.Append($", {ResponseLength} bytes");
if (ResponseTime != null)
sb.Append($", received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms");
return sb.ToString();
}
}
}
+8
View File
@@ -149,6 +149,14 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public class ServerError : Error
{
/// <summary>
/// ctor
/// </summary>
public ServerError(ErrorType type, string message, Exception? exception = null)
: base(null, new ErrorInfo(type, message), exception)
{
}
/// <summary>
/// ctor
/// </summary>
@@ -44,7 +44,7 @@ namespace CryptoExchange.Net.Objects.Options
/// <inheritdoc />
public override string ToString()
{
return $"RequestTimeout: {RequestTimeout}, Proxy: {(Proxy == null ? "-" : "set")}";
return $"Proxy: {(Proxy == null ? "-" : "set")}";
}
}
}
@@ -105,6 +105,12 @@ namespace CryptoExchange.Net.Objects.Options
target.Environment = Environment;
return target;
}
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()} | Environment: {Environment.Name}";
}
}
/// <inheritdoc />
@@ -131,7 +137,7 @@ namespace CryptoExchange.Net.Objects.Options
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
return $"{base.ToString()} | ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
}
}
@@ -132,6 +132,12 @@ namespace CryptoExchange.Net.Objects.Options
target.Environment = Environment;
return target;
}
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()} | Environment: {Environment.Name}";
}
}
/// <summary>
@@ -159,7 +165,7 @@ namespace CryptoExchange.Net.Objects.Options
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()}, ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
return $"{base.ToString()} | ApiCredentials: {(ApiCredentials == null ? "-" : "set")}";
}
}
}
@@ -1,343 +0,0 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters.SystemTextJson;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Parameters collection
/// </summary>
public class ParameterCollection : Dictionary<string, object>
{
/// <inheritdoc />
public new void Add(string key, object value)
{
if (value == null)
throw new ArgumentNullException(key);
base.Add(key, value);
}
/// <summary>
/// Add an optional parameter. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptional(string key, object? value)
{
if (value != null)
base.Add(key, value);
}
/// <summary>
/// Add a decimal value as string
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddString(string key, decimal value)
{
base.Add(key, value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a decimal value as string. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalString(string key, decimal? value)
{
if (value != null)
base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a int value as string
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddString(string key, int value)
{
base.Add(key, value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a int value as string. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalString(string key, int? value)
{
if (value != null)
base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a long value as string
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddString(string key, long value)
{
base.Add(key, value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a long value as string. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalString(string key, long? value)
{
if (value != null)
base.Add(key, value.Value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a DateTime value as string
/// </summary>
public void AddString(string key, DateTime value)
{
base.Add(key, value.ToString("yyyy-MM-ddTHH:mm:ssZ"));
}
/// <summary>
/// Add a DateTime value as string. Not added if value is null
/// </summary>
public void AddOptionalString(string key, DateTime? value)
{
if (value != null)
base.Add(key, value.Value.ToString("yyyy-MM-ddTHH:mm:ssZ"));
}
/// <summary>
/// Add a datetime value as milliseconds timestamp
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddMilliseconds(string key, DateTime value)
{
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value));
}
/// <summary>
/// Add a datetime value as milliseconds timestamp. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalMilliseconds(string key, DateTime? value)
{
if (value != null)
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value));
}
/// <summary>
/// Add a datetime value as milliseconds timestamp
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddMillisecondsString(string key, DateTime value)
{
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a datetime value as milliseconds timestamp. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalMillisecondsString(string key, DateTime? value)
{
if (value != null)
base.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Add a datetime value as seconds timestamp
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddSeconds(string key, DateTime value)
{
base.Add(key, DateTimeConverter.ConvertToSeconds(value));
}
/// <summary>
/// Add a datetime value as seconds timestamp. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalSeconds(string key, DateTime? value)
{
if (value != null)
base.Add(key, DateTimeConverter.ConvertToSeconds(value));
}
/// <summary>
/// Add a datetime value as string seconds timestamp
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddSecondsString(string key, DateTime value)
{
base.Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
}
/// <summary>
/// Add a datetime value as string seconds timestamp. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
public void AddOptionalSecondsString(string key, DateTime? value)
{
if (value != null)
base.Add(key, DateTimeConverter.ConvertToSeconds(value).ToString()!);
}
/// <summary>
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
/// </summary>
#if NET5_0_OR_GREATER
public void AddEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value)
#else
public void AddEnum<T>(string key, T value)
#endif
where T : struct, Enum
{
base.Add(key, EnumConverter<T>.GetString(value)!);
}
/// <summary>
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
#if NET5_0_OR_GREATER
public void AddEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value)
#else
public void AddEnumAsInt<T>(string key, T value)
#endif
where T : struct, Enum
{
var stringVal = EnumConverter<T>.GetString(value)!;
base.Add(key, int.Parse(stringVal)!);
}
/// <summary>
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
#if NET5_0_OR_GREATER
public void AddOptionalEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value)
#else
public void AddOptionalEnum<T>(string key, T? value)
#endif
where T : struct, Enum
{
if (value != null)
base.Add(key, EnumConverter<T>.GetString(value));
}
/// <summary>
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
/// </summary>
#if NET5_0_OR_GREATER
public void AddOptionalEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value)
#else
public void AddOptionalEnumAsInt<T>(string key, T? value)
#endif
where T : struct, Enum
{
if (value != null)
{
var stringVal = EnumConverter<T>.GetString(value);
base.Add(key, int.Parse(stringVal));
}
}
/// <summary>
/// Add key as comma separated values
/// </summary>
public void AddCommaSeparated(string key, IEnumerable<string> values)
{
base.Add(key, string.Join(",", values));
}
/// <summary>
/// Add key as comma separated values if there are values provided
/// </summary>
public void AddOptionalCommaSeparated(string key, IEnumerable<string>? values)
{
if (values == null || !values.Any())
return;
base.Add(key, string.Join(",", values));
}
/// <summary>
/// Add key as comma separated values
/// </summary>
#if NET5_0_OR_GREATER
public void AddCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T> values)
#else
public void AddCommaSeparated<T>(string key, IEnumerable<T> values)
#endif
where T : struct, Enum
{
base.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x))));
}
/// <summary>
/// Add key as comma separated values if there are values provided
/// </summary>
#if NET5_0_OR_GREATER
public void AddOptionalCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T>? values)
#else
public void AddOptionalCommaSeparated<T>(string key, IEnumerable<T>? values)
#endif
where T : struct, Enum
{
if (values == null || !values.Any())
return;
base.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x))));
}
/// <summary>
/// Add key as boolean lower case value
/// </summary>
public void AddBoolString(string key, bool value)
{
base.Add(key, value.ToString().ToLower());
}
/// <summary>
/// Add key as boolean lower case value if it's not null
/// </summary>
public void AddOptionalBoolString(string key, bool? value)
{
if (value == null)
return;
base.Add(key, value.ToString()!.ToLower());
}
/// <summary>
/// Set the request body. Can be used to specify a simple value or array as the body instead of an object
/// </summary>
/// <param name="body">Body to set</param>
/// <exception cref="InvalidOperationException"></exception>
public void SetBody(object body)
{
if (this.Any())
throw new InvalidOperationException("Can't set body when other parameters already specified");
base.Add(Constants.BodyPlaceHolderKey, body);
}
}
}
@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Settings for parameter serialization
/// </summary>
public class ParameterSerializationSettings
{
/// <summary>
/// Default serialization settings
/// </summary>
public static ParameterSerializationSettings Default { get; } = new ParameterSerializationSettings();
/// <summary>
/// Whether to sort the parameters
/// </summary>
public bool Sort { get; set; } = true;
/// <summary>
/// The parameter comparer when sorting
/// </summary>
public IComparer<string>? SortComparer { get; set; }
/// <summary>
/// Decimal serialization type
/// </summary>
public DecimalSerialization Decimal { get; set; } = DecimalSerialization.Number;
/// <summary>
/// DateTime serialization type
/// </summary>
public DateTimeSerialization DateTimes { get; set; } = DateTimeSerialization.MillisecondsNumber;
/// <summary>
/// Boolean serialization type
/// </summary>
public BoolSerialization Bool { get; set; } = BoolSerialization.Bool;
/// <summary>
/// Integer serialization type
/// </summary>
public IntegerSerialization Integer { get; set; } = IntegerSerialization.Number;
/// <summary>
/// Enum serialization type
/// </summary>
public EnumSerialization Enum { get; set; } = EnumSerialization.String;
/// <summary>
/// Array serialization type
/// </summary>
public ArrayParametersSerialization Array { get; set; } = ArrayParametersSerialization.Array;
}
/// <summary>
/// Type of decimal value serialization
/// </summary>
public enum DecimalSerialization
{
/// <summary>
/// Decimals should be serialized as numbers
/// </summary>
Number,
/// <summary>
/// Decimals should be strings
/// </summary>
String
}
/// <summary>
/// Type of DateTime value serialization
/// </summary>
public enum DateTimeSerialization
{
/// <summary>
/// DateTimes should be serialized as milliseconds number
/// </summary>
MillisecondsNumber,
/// <summary>
/// DateTimes should be serialized as milliseconds string
/// </summary>
MillisecondsString,
/// <summary>
/// DateTimes should be serialized as seconds number
/// </summary>
SecondsNumber,
/// <summary>
/// DateTimes should be serialized as seconds string
/// </summary>
SecondsString,
/// <summary>
/// DateTimes should be serialized as microseconds number
/// </summary>
MicrosecondsNumber,
/// <summary>
/// DateTimes should be serialized as microseconds string
/// </summary>
MicrosecondsString,
/// <summary>
/// DateTimes should be serialized as ISO 8601 string
/// </summary>
Rfc3339String
}
/// <summary>
/// Type of boolean value serialization
/// </summary>
public enum BoolSerialization
{
/// <summary>
/// Booleans should be serialized as bool values
/// </summary>
Bool,
/// <summary>
/// Booleans should be serialized as strings
/// </summary>
String
}
/// <summary>
/// Type of integer value serialization
/// </summary>
public enum IntegerSerialization
{
/// <summary>
/// Integers should be serialized as integer values
/// </summary>
Number,
/// <summary>
/// Integers should be serialized as strings
/// </summary>
String
}
/// <summary>
/// Type of enum value serialization
/// </summary>
public enum EnumSerialization
{
/// <summary>
/// Enums should be serialized as integer values
/// </summary>
Number,
/// <summary>
/// Enums should be serialized as strings
/// </summary>
String
}
}
+375
View File
@@ -0,0 +1,375 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Set of parameters
/// </summary>
public class Parameters : IDictionary<string, object>
{
private readonly ParameterSerializationSettings _serializationSettings;
private IDictionary<string, object> _parameters;
private object? _value;
/// <inheritdoc />
public object? BodyValue => _value;
/// <inheritdoc />
public ICollection<string> Keys => _parameters.Keys;
/// <inheritdoc />
public ICollection<object> Values => _parameters.Values;
/// <inheritdoc />
public int Count => _parameters.Count;
/// <inheritdoc />
public bool IsReadOnly => _parameters.IsReadOnly;
/// <summary>
/// Whether any parameters are defined
/// </summary>
public bool Empty => _parameters.Count == 0 && _value == null;
/// <inheritdoc />
public object this[string key] { get => _parameters[key]; set => _parameters[key] = value; }
/// <summary>
/// ctor
/// </summary>
/// <param name="serializationSettings">Serialization settings</param>
public Parameters(ParameterSerializationSettings serializationSettings)
{
_serializationSettings = serializationSettings;
if (_serializationSettings.Sort)
_parameters = new SortedDictionary<string, object>(_serializationSettings.SortComparer);
else
_parameters = new Dictionary<string, object>();
}
/// <summary>
/// ctor
/// </summary>
/// <param name="serializationSettings">Serialization settings</param>
/// <param name="value">Body value</param>
public Parameters(object value, ParameterSerializationSettings serializationSettings)
{
_parameters = new Dictionary<string, object>();
_serializationSettings = serializationSettings;
_value = value;
}
/// <summary>
/// Add a short value if it is not null
/// </summary>
public void Add(string key, short? value, IntegerSerialization? serialization = null)
{
if (value == null)
return;
Add(key, value.Value, serialization);
}
/// <summary>
/// Add a short value
/// </summary>
public void Add(string key, short value, IntegerSerialization? serialization = null)
{
var serializationToUse = serialization ?? _serializationSettings.Integer;
if (serializationToUse == IntegerSerialization.String)
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture));
else if (serializationToUse == IntegerSerialization.Number)
_parameters.Add(key, value);
else
throw new ArgumentException("Unknown Integer serialization setting");
}
/// <summary>
/// Add an int value if it is not null
/// </summary>
public void Add(string key, int? value, IntegerSerialization? serialization = null)
{
if (value == null)
return;
Add(key, value.Value, serialization);
}
/// <summary>
/// Add an int value
/// </summary>
public void Add(string key, int value, IntegerSerialization? serialization = null)
{
var serializationToUse = serialization ?? _serializationSettings.Integer;
if (serializationToUse == IntegerSerialization.String)
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture));
else if (serializationToUse == IntegerSerialization.Number)
_parameters.Add(key, value);
else
throw new ArgumentException("Unknown Integer serialization setting");
}
/// <summary>
/// Add a long value if it is not null
/// </summary>
public void Add(string key, long? value, IntegerSerialization? serialization = null)
{
if (value == null)
return;
Add(key, value.Value, serialization);
}
/// <summary>
/// Add a long value
/// </summary>
public void Add(string key, long value, IntegerSerialization? serialization = null)
{
var serializationToUse = serialization ?? _serializationSettings.Integer;
if (serializationToUse == IntegerSerialization.String)
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture));
else if (serializationToUse == IntegerSerialization.Number)
_parameters.Add(key, value);
else
throw new ArgumentException("Unknown Integer serialization setting");
}
/// <summary>
/// Add a decimal value if it is not null
/// </summary>
public void Add(string key, decimal? value, DecimalSerialization? serialization = null)
{
if (value == null)
return;
Add(key, value.Value, serialization);
}
/// <summary>
/// Add a decimal value
/// </summary>
public void Add(string key, decimal value, DecimalSerialization? serialization = null)
{
var serializationToUse = serialization ?? _serializationSettings.Decimal;
if (serializationToUse == DecimalSerialization.String)
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture));
else if (serializationToUse == DecimalSerialization.Number)
_parameters.Add(key, value);
else
throw new ArgumentException("Unknown Decimal serialization setting");
}
/// <summary>
/// Add a double value if it is not null
/// </summary>
public void Add(string key, double? value, DecimalSerialization? serialization = null)
{
if (value == null)
return;
Add(key, value.Value, serialization);
}
/// <summary>
/// Add a double value
/// </summary>
public void Add(string key, double value, DecimalSerialization? serialization = null)
{
var serializationToUse = serialization ?? _serializationSettings.Decimal;
if (serializationToUse == DecimalSerialization.String)
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture));
else if (serializationToUse == DecimalSerialization.Number)
_parameters.Add(key, value);
else
throw new ArgumentException("Unknown Decimal serialization setting");
}
/// <summary>
/// Add a bool value if it is not null
/// </summary>
public void Add(string key, bool? value, BoolSerialization? serialization = null)
{
if (value == null)
return;
Add(key, value.Value, serialization);
}
/// <summary>
/// Add a bool value
/// </summary>
public void Add(string key, bool value, BoolSerialization? serialization = null)
{
var serializationToUse = serialization ?? _serializationSettings.Bool;
if (serializationToUse == BoolSerialization.String)
_parameters.Add(key, value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
else if (serializationToUse == BoolSerialization.Bool)
_parameters.Add(key, value);
else
throw new ArgumentException("Unknown Bool serialization setting");
}
/// <summary>
/// Add key as comma separated values if there are values provided
/// </summary>
public void AddCommaSeparated(string key, IEnumerable<string>? values)
{
if (values == null || !values.Any())
return;
_parameters.Add(key, string.Join(",", values));
}
/// <summary>
/// Add key as comma separated values
/// </summary>
#if NET5_0_OR_GREATER
public void AddCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T> values)
#else
public void AddCommaSeparated<T>(string key, IEnumerable<T>? values)
#endif
where T : struct, Enum
{
if (values == null || !values.Any())
return;
_parameters.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x))));
}
/// <summary>
/// Add an enum value if it is not null
/// </summary>
public void Add<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)]
# endif
T>(string key, T? value, EnumSerialization? serialization = null)
where T : struct, Enum
{
if (value == null)
return;
Add(key, value.Value, serialization);
}
/// <summary>
/// Add a enum value
/// </summary>
public void Add<
#if NET5_0_OR_GREATER
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)]
#endif
T>(string key, T value, EnumSerialization? serialization = null)
where T : struct, Enum
{
var serializationToUse = serialization ?? _serializationSettings.Enum;
if (serializationToUse == EnumSerialization.String)
_parameters.Add(key, EnumConverter<T>.GetString(value));
else if (serializationToUse == EnumSerialization.Number)
_parameters.Add(key, int.Parse(EnumConverter<T>.GetString(value), CultureInfo.InvariantCulture));
else
throw new ArgumentException("Unknown Integer serialization setting");
}
/// <summary>
/// Add a DateTime value if it is not null
/// </summary>
public void Add(string key, DateTime? value, DateTimeSerialization? serialization = null)
{
if (value == null)
return;
Add(key, value.Value, serialization);
}
/// <summary>
/// Add a DateTime value
/// </summary>
public void Add(string key, DateTime value, DateTimeSerialization? serialization = null)
{
var serializationToUse = serialization ?? _serializationSettings.DateTimes;
if (serializationToUse == DateTimeSerialization.MillisecondsNumber)
_parameters.Add(key, DateTimeConverter.ConvertToMilliseconds(value));
else if (serializationToUse == DateTimeSerialization.MillisecondsString)
_parameters.Add(key, DateTimeConverter.ConvertToMilliseconds(value).Value.ToString(CultureInfo.InvariantCulture));
else if (serializationToUse == DateTimeSerialization.SecondsNumber)
_parameters.Add(key, DateTimeConverter.ConvertToSeconds(value));
else if (serializationToUse == DateTimeSerialization.SecondsString)
_parameters.Add(key, DateTimeConverter.ConvertToSeconds(value).Value.ToString(CultureInfo.InvariantCulture));
else if (serializationToUse == DateTimeSerialization.MicrosecondsNumber)
_parameters.Add(key, DateTimeConverter.ConvertToMicroseconds(value));
else if (serializationToUse == DateTimeSerialization.MicrosecondsString)
_parameters.Add(key, DateTimeConverter.ConvertToMicroseconds(value).Value.ToString(CultureInfo.InvariantCulture));
else if (serializationToUse == DateTimeSerialization.Rfc3339String)
_parameters.Add(key, value.ToRfc3339String());
else
throw new ArgumentException("Unknown DateTime serialization setting");
}
/// <summary>
/// Add a string value if it is not null
/// </summary>
public void Add(string key, string? value)
{
if (value == null)
return;
_parameters.Add(key, value);
}
/// <summary>
/// Add an array of values if there are values provided
/// </summary>
public void AddArray<T>(string key, IEnumerable<T>? values)
{
if (values == null || !values.Any())
return;
_parameters.Add(key, values is T[] arr ? arr : values.ToArray());
}
/// <summary>
/// Add a raw object value if it is not null
/// </summary>
public void AddRaw(string key, object? value)
{
if (value == null)
return;
_parameters.Add(key, value);
}
/// <inheritdoc />
public void Add(string key, object value) => _parameters.Add(key, value);
/// <inheritdoc />
public bool ContainsKey(string key) => _parameters.ContainsKey(key);
/// <inheritdoc />
public bool Remove(string key) => _parameters.Remove(key);
/// <inheritdoc />
public bool TryGetValue(string key, out object value) => _parameters.TryGetValue(key, out value!);
/// <inheritdoc />
public void Add(KeyValuePair<string, object> item) => _parameters.Add(item.Key, item.Value);
/// <inheritdoc />
public void Clear() => _parameters.Clear();
/// <inheritdoc />
public bool Contains(KeyValuePair<string, object> item) => _parameters.ContainsKey(item.Key) && _parameters[item.Key] == item.Value;
/// <inheritdoc />
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex) => _parameters.CopyTo(array, arrayIndex);
/// <inheritdoc />
public bool Remove(KeyValuePair<string, object> item) => _parameters.Remove(item.Key);
/// <inheritdoc />
public IEnumerator<KeyValuePair<string, object>> GetEnumerator() => _parameters.GetEnumerator();
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
+14 -1
View File
@@ -33,11 +33,23 @@
/// Centralization type
/// </summary>
public CentralizationType CentralizationType { get; }
/// <summary>
/// Supported environments
/// </summary>
public string[] SupportedEnvironments { get; }
/// <summary>
/// ctor
/// </summary>
public PlatformInfo(string id, string displayName, string logo, string url, string[] apiDocsUrl, PlatformType platformType, CentralizationType centralizationType)
public PlatformInfo(
string id,
string displayName,
string logo,
string url,
string[] apiDocsUrl,
PlatformType platformType,
CentralizationType centralizationType,
string[] supportedEnvironments)
{
Id = id;
DisplayName = displayName;
@@ -46,6 +58,7 @@
ApiDocsUrl = apiDocsUrl;
PlatformType = platformType;
CentralizationType = centralizationType;
SupportedEnvironments = supportedEnvironments;
}
}
}
@@ -9,9 +9,14 @@ namespace CryptoExchange.Net.Objects
public class RequestDefinition
{
private string? _stringRep;
private string? _fullUrl;
// Basics
/// <summary>
/// Base address of the request
/// </summary>
public string BaseAddress { get; set; }
/// <summary>
/// Path of the request
/// </summary>
@@ -77,13 +82,31 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public bool? ForcePathEndWithSlash { get; set; }
/// <summary>
/// Full url, host + path
/// </summary>
public string FullUrl
{
get
{
if (_fullUrl != null)
return _fullUrl;
var result = BaseAddress.AppendPath(Path);
if (ForcePathEndWithSlash == true && !result.EndsWith("/"))
result += "/";
_fullUrl = result;
return _fullUrl;
}
}
/// <summary>
/// ctor
/// </summary>
/// <param name="path"></param>
/// <param name="method"></param>
public RequestDefinition(string path, HttpMethod method)
public RequestDefinition(string baseAddress, string path, HttpMethod method)
{
BaseAddress = baseAddress;
Path = path;
Method = method;
@@ -1,5 +1,6 @@
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Collections.Concurrent;
using System.IO;
using System.Net.Http;
namespace CryptoExchange.Net.Objects
@@ -15,27 +16,30 @@ namespace CryptoExchange.Net.Objects
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
/// </summary>
/// <param name="method">The HttpMethod</param>
/// <param name="baseAddress">The base address/host</param>
/// <param name="path">Endpoint path</param>
/// <param name="authenticated">Endpoint is authenticated</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(HttpMethod method, string path, bool authenticated = false)
=> GetOrCreate(method, path, null, 0, authenticated, null, null, null, null, null);
public RequestDefinition GetOrCreate(HttpMethod method, string baseAddress, string path, bool authenticated = false)
=> GetOrCreate(method, baseAddress, path, null, 0, authenticated, null, null, null, null, null, null, null);
/// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
/// </summary>
/// <param name="method">The HttpMethod</param>
/// <param name="baseAddress">The base address/host</param>
/// <param name="path">Endpoint path</param>
/// <param name="rateLimitGate">The rate limit gate</param>
/// <param name="weight">Request weight</param>
/// <param name="authenticated">Endpoint is authenticated</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(HttpMethod method, string path, IRateLimitGate rateLimitGate, int weight = 1, bool authenticated = false)
=> GetOrCreate(method, path, rateLimitGate, weight, authenticated, null, null, null, null, null);
public RequestDefinition GetOrCreate(HttpMethod method, string baseAddress, string path, IRateLimitGate rateLimitGate, int weight = 1, bool authenticated = false)
=> GetOrCreate(method, baseAddress, path, rateLimitGate, weight, authenticated, null, null, null, null, null, null, null);
/// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
/// </summary>
/// <param name="baseAddress">The base address/host</param>
/// <param name="method">The HttpMethod</param>
/// <param name="path">Endpoint path</param>
/// <param name="rateLimitGate">The rate limit gate</param>
@@ -48,9 +52,11 @@ namespace CryptoExchange.Net.Objects
/// <param name="preventCaching">Prevent request caching</param>
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
/// <param name="forcePathEndWithSlash">Force trailing `/`</param>
/// <param name="identifier">Optional request identifier override</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(
HttpMethod method,
string baseAddress,
string path,
IRateLimitGate? rateLimitGate,
int weight,
@@ -61,45 +67,13 @@ namespace CryptoExchange.Net.Objects
ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null,
bool? tryParseOnNonSuccess = null,
bool? forcePathEndWithSlash = null)
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess, forcePathEndWithSlash);
/// <summary>
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
/// </summary>
/// <param name="identifier">Request identifier</param>
/// <param name="method">The HttpMethod</param>
/// <param name="path">Endpoint path</param>
/// <param name="rateLimitGate">The rate limit gate</param>
/// <param name="limitGuard">The rate limit guard for this specific endpoint</param>
/// <param name="weight">Request weight</param>
/// <param name="authenticated">Endpoint is authenticated</param>
/// <param name="requestBodyFormat">Request body format</param>
/// <param name="parameterPosition">Parameter position</param>
/// <param name="arraySerialization">Array serialization type</param>
/// <param name="preventCaching">Prevent request caching</param>
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
/// <param name="forcePathEndWithSlash">Force trailing `/`</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(
string identifier,
HttpMethod method,
string path,
IRateLimitGate? rateLimitGate,
int weight,
bool authenticated,
IRateLimitGuard? limitGuard = null,
RequestBodyFormat? requestBodyFormat = null,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
bool? preventCaching = null,
bool? tryParseOnNonSuccess = null,
bool? forcePathEndWithSlash = null)
bool? forcePathEndWithSlash = null,
string? identifier = null)
{
if (!_definitions.TryGetValue(identifier, out var def))
var identifierToUse = identifier ?? $"{path}{method.Method}{baseAddress}";
if (!_definitions.TryGetValue(identifierToUse, out var def))
{
def = new RequestDefinition(path, method)
def = new RequestDefinition(baseAddress, path, method)
{
Authenticated = authenticated,
LimitGuard = limitGuard,
@@ -110,9 +84,9 @@ namespace CryptoExchange.Net.Objects
ParameterPosition = parameterPosition,
PreventCaching = preventCaching ?? false,
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false,
ForcePathEndWithSlash = forcePathEndWithSlash ?? false
ForcePathEndWithSlash = forcePathEndWithSlash ?? false,
};
_definitions.TryAdd(identifier, def);
_definitions.TryAdd(identifierToUse, def);
}
return def;
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using CryptoExchange.Net.Interfaces;
using System.Collections.Generic;
using System.Net.Http;
namespace CryptoExchange.Net.Objects
@@ -12,29 +13,17 @@ namespace CryptoExchange.Net.Objects
private string? _queryString;
/// <summary>
/// Http method
/// The request definition for the request
/// </summary>
public HttpMethod Method { get; set; }
/// <summary>
/// Whether the request needs authentication
/// </summary>
public bool Authenticated { get; set; }
/// <summary>
/// Base address for the request
/// </summary>
public string BaseAddress { get; set; }
/// <summary>
/// The request path
/// </summary>
public string Path { get; set; }
public RequestDefinition RequestDefinition { get; set; }
/// <summary>
/// Query parameters
/// </summary>
public IDictionary<string, object>? QueryParameters { get; set; }
public Parameters? QueryParameters { get; set; }
/// <summary>
/// Body parameters
/// </summary>
public IDictionary<string, object>? BodyParameters { get; set; }
public Parameters? BodyParameters { get; set; }
/// <summary>
/// Request headers
/// </summary>
@@ -57,22 +46,16 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public RestRequestConfiguration(
RequestDefinition requestDefinition,
string baseAddress,
IDictionary<string, object>? queryParams,
IDictionary<string, object>? bodyParams,
Parameters? queryParams,
Parameters? bodyParams,
IDictionary<string, string>? headers,
ArrayParametersSerialization arraySerialization,
HttpMethodParameterPosition parametersPosition,
RequestBodyFormat bodyFormat)
{
Method = requestDefinition.Method;
Authenticated = requestDefinition.Authenticated;
Path = requestDefinition.Path;
BaseAddress = baseAddress;
RequestDefinition = requestDefinition;
QueryParameters = queryParams;
BodyParameters = bodyParams;
Headers = headers;
ArraySerialization = arraySerialization;
ParameterPosition = parametersPosition;
BodyFormat = bodyFormat;
}
@@ -80,15 +63,15 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// Get the parameter collection based on the ParameterPosition
/// </summary>
public IDictionary<string, object> GetPositionParameters()
public Parameters GetPositionParameters()
{
if (ParameterPosition == HttpMethodParameterPosition.InBody)
{
BodyParameters ??= new Dictionary<string, object>();
BodyParameters ??= new Parameters(ParameterSerializationSettings.Default);
return BodyParameters;
}
QueryParameters ??= new Dictionary<string, object>();
QueryParameters ??= new Parameters(ParameterSerializationSettings.Default);
return QueryParameters;
}
@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace CryptoExchange.Net.Objects;
/// <summary>
/// Call result
/// </summary>
public record CallResult : ICallResult
{
private static CallResult _successResult = new CallResult();
/// <inheritdoc />
public Error? Error { get; init; }
/// <inheritdoc />
[MemberNotNullWhen(false, nameof(Error))]
public bool Success => Error == null;
/// <summary>
/// Create an error response
/// </summary>
/// <param name="error">The error</param>
public static CallResult Fail(Error error) => new CallResult { Error = error };
/// <summary>
/// Create a success result
/// </summary>
public static CallResult Ok() => _successResult;
/// <summary>
/// Create a success result
/// </summary>
/// <typeparam name="T">Result type</typeparam>
/// <param name="originalData">The original string data</param>
/// <param name="data">Data type</param>
public static CallResult<T> Ok<T>(T data, string? originalData = null) => new CallResult<T> { Data = data, OriginalData = originalData };
/// <summary>
/// Create an error response
/// </summary>
/// <typeparam name="T">Result type</typeparam>
/// <param name="originalData">The original string data</param>
/// <param name="error">The error</param>
public static CallResult<T> Fail<T>(Error error, string? originalData = null) => new CallResult<T> { Error = error, OriginalData = originalData };
/// <inheritdoc />
public override string ToString()
{
return Success ? $"Success" : $"Error: {Error}";
}
}
/// <inheritdoc />
public record CallResult<T> : CallResult, ICallResult<T>
{
/// <inheritdoc />
public new Error? Error
{
get => base.Error;
init => base.Error = value;
}
/// <inheritdoc />
[MemberNotNullWhen(false, nameof(Error))]
[MemberNotNullWhen(true, nameof(Data))]
public new bool Success => Error == null;
/// <summary>
/// The data returned by the call, only available when Success = true
/// </summary>
public T? Data { get; init; }
/// <summary>
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
/// </summary>
public string? OriginalData { get; init; }
/// <summary>
/// Create an error response
/// </summary>
/// <param name="error">The error</param>
/// <param name="originalData">The original string data</param>
public static CallResult<T> Fail(Error error, string? originalData = null) => new CallResult<T> { Error = error, OriginalData = originalData };
/// <summary>
/// Create a success result
/// </summary>
/// <param name="data">The data</param>
/// <param name="originalData">The original string data</param>
/// <returns></returns>
public static CallResult<T> Ok(T data, string? originalData = null) => new CallResult<T> { Data = data, OriginalData = originalData };
}
/// <summary>
/// Call result for an exchange
/// </summary>
/// <typeparam name="T">Data type</typeparam>
public record ExchangeCallResult<T> : CallResult<T>
{
/// <summary>
/// Exchange name
/// </summary>
public string Exchange { get; set; } = string.Empty;
/// <summary>
/// Create an error response
/// </summary>
/// <param name="exchange">The exchange name</param>
/// <param name="error">The error</param>
/// <param name="originalData">The original string data</param>
public static ExchangeCallResult<T> Fail(string exchange, Error error, string? originalData = null) => new ExchangeCallResult<T> { Exchange = exchange, Error = error };
/// <summary>
/// Create a success result
/// </summary>
/// <param name="exchange">The exchange name</param>
/// <param name="data">The data</param>
/// <param name="originalData">The original string data</param>
/// <returns></returns>
public static ExchangeCallResult<T> Ok(string exchange, T data, string? originalData = null) => new ExchangeCallResult<T> { Exchange = exchange, Data = data };
}
@@ -0,0 +1,286 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
namespace CryptoExchange.Net.Objects;
/// <summary>
/// HTTP call result
/// </summary>
public record HttpResult : IHttpResult
{
/// <summary>
/// Create a new success HTTP result
/// </summary>
public static HttpResult<T> Ok<T>(
string exchange,
HttpStatusCode code,
Version version,
HttpResponseHeaders responseHeaders,
TimeSpan elapsed,
long? contentLength,
string? originalData,
int requestId,
string uri,
string? content,
HttpMethod method,
HttpRequestHeaders requestHeaders,
ResultDataSource source,
T data) =>
new HttpResult<T>(exchange, data, null)
{
ResponseStatusCode = code,
HttpVersion = version,
ResponseHeaders = responseHeaders,
ResponseTime = elapsed,
ResponseLength = contentLength,
OriginalData = originalData,
RequestId = requestId,
RequestUrl = uri,
RequestBody = content,
RequestMethod = method,
RequestHeaders = requestHeaders,
DataSource = source,
};
/// <summary>
/// Create a new success HTTP result
/// </summary>
public static HttpResult<T> Ok<T>(IHttpResult result, T data, PageRequest? pageRequest = null) =>
new HttpResult<T>(result.Exchange, data, null)
{
ResponseStatusCode = result.ResponseStatusCode,
HttpVersion = result.HttpVersion,
ResponseHeaders = result.ResponseHeaders,
ResponseTime = result.ResponseTime,
ResponseLength = result.ResponseLength,
OriginalData = result.OriginalData,
RequestId = result.RequestId,
RequestUrl = result.RequestUrl,
RequestBody = result.RequestBody,
RequestMethod = result.RequestMethod,
RequestHeaders = result.RequestHeaders,
DataSource = result.DataSource,
Error = result.Error,
Data = data,
NextPageRequest = pageRequest
};
/// <summary>
/// Create a new error HTTP result
/// </summary>
public static HttpResult<T> Fail<T>(string exchange, Error error) => new HttpResult<T>(exchange, default, error);
/// <summary>
/// Create a new error HTTP result
/// </summary>
public static HttpResult<T> Fail<T>(IHttpResult result, Error? error = null, T? data = default)
=> new HttpResult<T>(result.Exchange, data, error ?? result.Error)
{
ResponseStatusCode = result.ResponseStatusCode,
HttpVersion = result.HttpVersion,
ResponseHeaders = result.ResponseHeaders,
ResponseTime = result.ResponseTime,
ResponseLength = result.ResponseLength,
OriginalData = result.OriginalData,
RequestId = result.RequestId,
RequestUrl = result.RequestUrl,
RequestBody = result.RequestBody,
RequestMethod = result.RequestMethod,
RequestHeaders = result.RequestHeaders,
DataSource = result.DataSource,
};
/// <summary>
/// Create a new error HTTP result
/// </summary>
public static HttpResult<T> Fail<T>(
string exchange,
HttpStatusCode? code,
Version? version,
HttpResponseHeaders? responseHeaders,
TimeSpan elapsed,
long? contentLength,
string? originalData,
int requestId,
string uri,
string? content,
HttpMethod method,
HttpRequestHeaders requestHeaders,
ResultDataSource source,
Error error,
T? result = default) =>
new HttpResult<T>(exchange, result, error)
{
ResponseStatusCode = code,
HttpVersion = version,
ResponseHeaders = responseHeaders,
ResponseTime = elapsed,
ResponseLength = contentLength,
OriginalData = originalData,
RequestId = requestId,
RequestUrl = uri,
RequestBody = content,
RequestMethod = method,
RequestHeaders = requestHeaders,
DataSource = source,
};
/// <summary>
/// Create a new error HTTP result
/// </summary>
public static HttpResult Fail(string exchange, Error error) => new HttpResult() { Exchange = exchange, Error = error };
/// <summary>
/// Create a new error HTTP result
/// </summary>
public static HttpResult Fail(IHttpResult result, Error? error = null)
=> new HttpResult()
{
ResponseStatusCode = result.ResponseStatusCode,
HttpVersion = result.HttpVersion,
ResponseHeaders = result.ResponseHeaders,
ResponseTime = result.ResponseTime,
ResponseLength = result.ResponseLength,
OriginalData = result.OriginalData,
RequestId = result.RequestId,
RequestUrl = result.RequestUrl,
RequestBody = result.RequestBody,
RequestMethod = result.RequestMethod,
RequestHeaders = result.RequestHeaders,
DataSource = result.DataSource,
Exchange = result.Exchange,
Error = error ?? result.Error
};
/// <summary>
/// Create a new success HTTP result
/// </summary>
public static HttpResult Ok(IHttpResult result)
=> new HttpResult()
{
ResponseStatusCode = result.ResponseStatusCode,
HttpVersion = result.HttpVersion,
ResponseHeaders = result.ResponseHeaders,
ResponseTime = result.ResponseTime,
ResponseLength = result.ResponseLength,
OriginalData = result.OriginalData,
RequestId = result.RequestId,
RequestUrl = result.RequestUrl,
RequestBody = result.RequestBody,
RequestMethod = result.RequestMethod,
RequestHeaders = result.RequestHeaders,
DataSource = result.DataSource,
Exchange = result.Exchange,
};
/// <summary>
/// Exchange name
/// </summary>
public string Exchange { get; init; } = string.Empty;
/// <inheritdoc />
public Error? Error { get; internal set; }
/// <inheritdoc />
[MemberNotNullWhen(false, nameof(Error))]
public bool Success => Error == null;
/// <summary>
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
/// </summary>
public string? OriginalData { get; init; }
/// <summary>
/// The request http method
/// </summary>
public HttpMethod? RequestMethod { get; init; }
/// <summary>
/// HTTP protocol version
/// </summary>
public Version? HttpVersion { get; init; }
/// <summary>
/// The headers sent with the request
/// </summary>
public HttpRequestHeaders? RequestHeaders { get; init; }
/// <summary>
/// The request id
/// </summary>
public int? RequestId { get; init; }
/// <summary>
/// The url which was requested
/// </summary>
public string? RequestUrl { get; init; }
/// <summary>
/// The body of the request
/// </summary>
public string? RequestBody { get; init; }
/// <summary>
/// Length in bytes of the response
/// </summary>
public long? ResponseLength { get; init; }
/// <summary>
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
/// </summary>
public HttpStatusCode? ResponseStatusCode { get; init; }
/// <summary>
/// The response headers
/// </summary>
public HttpResponseHeaders? ResponseHeaders { get; init; }
/// <summary>
/// The time between sending the request and receiving the response
/// </summary>
public TimeSpan? ResponseTime { get; init; }
/// <summary>
/// The data source of this result
/// </summary>
public ResultDataSource DataSource { get; init; } = ResultDataSource.Server;
}
/// <inheritdoc />
public record HttpResult<T> : HttpResult, IHttpResult<T>
{
/// <summary>
/// ctor
/// </summary>
public HttpResult(string exchange, T? value, Error? error)
{
Exchange = exchange;
Data = value;
Error = error;
}
/// <inheritdoc />
public new Error? Error
{
get => base.Error;
internal set => base.Error = value;
}
/// <inheritdoc />
[MemberNotNullWhen(false, nameof(Error))]
[MemberNotNullWhen(true, nameof(Data))]
public new bool Success => Error == null;
/// <summary>
/// The data returned by the call, only available when Success = true
/// </summary>
public T? Data { get; init; }
/// <summary>
/// Next page request, only potentially available when using Shared API's
/// </summary>
public PageRequest? NextPageRequest { get; init; }
}
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace CryptoExchange.Net.Objects;
/// <summary>
/// Call result
/// </summary>
public interface ICallResult
{
/// <summary>
/// An error if the call didn't succeed, will always be filled if Success = false
/// </summary>
Error? Error { get; }
/// <summary>
/// Whether the call was successful
/// </summary>
[MemberNotNullWhen(false, nameof(Error))]
bool Success { get; }
}
/// <summary>
/// Call result
/// </summary>
/// <typeparam name="T">Result data type</typeparam>
public interface ICallResult<T> : ICallResult
{
/// <inheritdoc />
new Error? Error { get; }
/// <inheritdoc />
[MemberNotNullWhen(false, nameof(Error))]
[MemberNotNullWhen(true, nameof(Data))]
new bool Success { get; }
/// <summary>
/// The result data, only available when Success = true
/// </summary>
T? Data { get; }
}
@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// HTTP call result
/// </summary>
public interface IHttpResult : ICallResult
{
/// <summary>
/// Exchange name
/// </summary>
string Exchange { get; init; }
/// <summary>
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
/// </summary>
string? OriginalData { get; init; }
/// <summary>
/// The request http method
/// </summary>
HttpMethod? RequestMethod { get; init; }
/// <summary>
/// HTTP protocol version
/// </summary>
Version? HttpVersion { get; init; }
/// <summary>
/// The headers sent with the request
/// </summary>
HttpRequestHeaders? RequestHeaders { get; init; }
/// <summary>
/// The request id
/// </summary>
int? RequestId { get; init; }
/// <summary>
/// The url which was requested
/// </summary>
string? RequestUrl { get; init; }
/// <summary>
/// The body of the request
/// </summary>
string? RequestBody { get; init; }
/// <summary>
/// Length in bytes of the response
/// </summary>
long? ResponseLength { get; init; }
/// <summary>
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
/// </summary>
HttpStatusCode? ResponseStatusCode { get; init; }
/// <summary>
/// The response headers
/// </summary>
HttpResponseHeaders? ResponseHeaders { get; init; }
/// <summary>
/// The time between sending the request and receiving the response
/// </summary>
TimeSpan? ResponseTime { get; init; }
/// <summary>
/// The data source of this result
/// </summary>
ResultDataSource DataSource { get; init; }
}
/// <summary>
/// HTTP call result
/// </summary>
/// <typeparam name="T">Result data type</typeparam>
public interface IHttpResult<T> : IHttpResult, ICallResult<T>
{
}
}
@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// WebSocket call result
/// </summary>
public interface IWebSocketResult : ICallResult
{
/// <summary>
/// Exchange name
/// </summary>
string Exchange { get; init; }
/// <summary>
/// The request id
/// </summary>
public int? RequestId { get; init; }
/// <summary>
/// The url which was requested
/// </summary>
public int? ConnectionId { get; init; }
/// <summary>
/// The websocket url
/// </summary>
public string? Url { get; init; }
/// <summary>
/// The time between sending the request and receiving the response
/// </summary>
public TimeSpan? ResponseTime { get; init; }
}
/// <summary>
/// WebSocket call result
/// </summary>
/// <typeparam name="T">Data result type</typeparam>
public interface IWebSocketResult<T> : IWebSocketResult, ICallResult<T>
{
}
/// <summary>
/// Query result
/// </summary>
public interface IQueryResult : IWebSocketResult
{
/// <summary>
/// The original returned data, only available when OutputOriginalData is set to true in the client options
/// </summary>
public string? OriginalData { get; init; }
/// <summary>
/// The query request body
/// </summary>
public string? RequestBody { get; init; }
}
/// <summary>
/// Query result
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IQueryResult<T> : IQueryResult, IWebSocketResult<T>
{
}
}
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Objects;
/// <summary>
/// Void result
/// </summary>
public readonly struct Unit
{
/// <summary>
/// Void value
/// </summary>
public static readonly Unit Value = default;
/// <summary>
/// Type
/// </summary>
public static Type Type { get; } = typeof(Unit);
}
@@ -0,0 +1,320 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace CryptoExchange.Net.Objects;
/// <summary>
/// WebSocket call result
/// </summary>
public record WebSocketResult : IWebSocketResult
{
/// <summary>
/// ctor
/// </summary>
public WebSocketResult(string exchange, Error? error)
{
Exchange = exchange;
Error = error;
}
/// <summary>
/// Create a new success WebSocket result
/// </summary>
public static WebSocketResult<T> Ok<T>(IWebSocketResult result, T data) =>
new WebSocketResult<T>(result.Exchange, data, null)
{
ConnectionId = result.ConnectionId,
Url = result.Url,
RequestId = result.RequestId,
ResponseTime = result.ResponseTime,
Error = result.Error,
Data = data
};
/// <summary>
/// Create a new success WebSocket result
/// </summary>
public static WebSocketResult<T> Ok<T>(
string exchange,
int connectionId,
TimeSpan elapsed,
int requestId,
string? url,
T data) =>
new WebSocketResult<T>(exchange, data, null)
{
ResponseTime = elapsed,
RequestId = requestId,
ConnectionId = connectionId,
Url = url
};
/// <summary>
/// Create a new success WebSocket result
/// </summary>
public static WebSocketResult Ok(
string exchange,
int connectionId,
TimeSpan elapsed,
int requestId,
string? url) =>
new WebSocketResult(exchange, null)
{
ResponseTime = elapsed,
RequestId = requestId,
ConnectionId = connectionId,
Url = url
};
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public static WebSocketResult Fail(string exchange, Error error) => new WebSocketResult(exchange, error);
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public static WebSocketResult Fail(
string exchange,
int connectionId,
TimeSpan elapsed,
int requestId,
string? url,
Error error) =>
new WebSocketResult(exchange, error)
{
ResponseTime = elapsed,
RequestId = requestId,
ConnectionId = connectionId,
Url = url
};
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public static WebSocketResult<T> Fail<T>(IWebSocketResult result, Error? error = null, T? data = default)
=> new WebSocketResult<T>(result.Exchange, data, error ?? result.Error)
{
ConnectionId = result.ConnectionId,
Url = result.Url,
RequestId = result.RequestId,
ResponseTime = result.ResponseTime,
};
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public static WebSocketResult<T> Fail<T>(string exchange, Error error) => new WebSocketResult<T>(exchange, default, error);
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public static WebSocketResult<T> Fail<T>(
string exchange,
int connectionId,
TimeSpan elapsed,
int requestId,
string? url,
Error error) =>
new WebSocketResult<T>(exchange, default, error)
{
ResponseTime = elapsed,
RequestId = requestId,
ConnectionId = connectionId,
Url = url
};
/// <summary>
/// Exchange name
/// </summary>
public string Exchange { get; init; }
/// <inheritdoc />
public Error? Error { get; init; }
/// <inheritdoc />
[MemberNotNullWhen(false, nameof(Error))]
public bool Success => Error == null;
/// <summary>
/// The request id
/// </summary>
public int? RequestId { get; init; }
/// <summary>
/// The url which was requested
/// </summary>
public int? ConnectionId { get; init; }
/// <summary>
/// The websocket url
/// </summary>
public string? Url { get; init; }
/// <summary>
/// The time between sending the request and receiving the response
/// </summary>
public TimeSpan? ResponseTime { get; init; }
}
/// <inheritdoc />
public record WebSocketResult<T> : WebSocketResult, IWebSocketResult<T>
{
/// <summary>
/// ctor
/// </summary>
public WebSocketResult(string exchange, T? value, Error? error): base(exchange, error)
{
Data = value;
}
/// <inheritdoc />
public new Error? Error
{
get => base.Error;
init => base.Error = value;
}
/// <inheritdoc />
[MemberNotNullWhen(false, nameof(Error))]
[MemberNotNullWhen(true, nameof(Data))]
public new bool Success => Error == null;
/// <summary>
/// The data returned by the call, only available when Success = true
/// </summary>
public T? Data { get; init; }
}
/// <inheritdoc />
public record QueryResult : WebSocketResult
{
/// <summary>
/// ctor
/// </summary>
public QueryResult(string exchange, Error? error) : base(exchange, error)
{
}
/// <summary>
/// Create a new error Query result
/// </summary>
public new static QueryResult Fail(string exchange, Error error) => new QueryResult(exchange, error);
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public static QueryResult Fail(IQueryResult result, Error? error = null)
=> new QueryResult(result.Exchange, error ?? result.Error)
{
ConnectionId = result.ConnectionId,
Url = result.Url,
RequestId = result.RequestId,
ResponseTime = result.ResponseTime,
};
/// <summary>
/// Create a new success query result
/// </summary>
public static QueryResult<T> Ok<T>(
string exchange,
int connectionId,
TimeSpan elapsed,
int requestId,
string? requestBody,
string? url,
string? originalData,
T data) =>
new QueryResult<T>(exchange, data, null)
{
ResponseTime = elapsed,
RequestId = requestId,
RequestBody = requestBody,
ConnectionId = connectionId,
Url = url,
OriginalData = originalData,
};
/// <summary>
/// Create a new success WebSocket result
/// </summary>
public static QueryResult<T> Ok<T>(IQueryResult result, T data) =>
new QueryResult<T>(result.Exchange, data, null)
{
ConnectionId = result.ConnectionId,
Url = result.Url,
RequestId = result.RequestId,
RequestBody = result.RequestBody,
ResponseTime = result.ResponseTime,
Error = result.Error,
OriginalData = result.OriginalData,
Data = data
};
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public static QueryResult<T> Fail<T>(
string exchange,
int connectionId,
TimeSpan elapsed,
int requestId,
string? requestBody,
string? url,
string? originalData,
Error error) =>
new QueryResult<T>(exchange, default, error)
{
ResponseTime = elapsed,
RequestId = requestId,
RequestBody = requestBody,
ConnectionId = connectionId,
OriginalData = originalData,
Url = url
};
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public static QueryResult<T> Fail<T>(IQueryResult result, Error? error = null, T? data = default)
=> new QueryResult<T>(result.Exchange, data, error ?? result.Error)
{
ConnectionId = result.ConnectionId,
Url = result.Url,
RequestId = result.RequestId,
RequestBody = result.RequestBody,
OriginalData = result.OriginalData,
ResponseTime = result.ResponseTime,
};
/// <summary>
/// Create a new error WebSocket result
/// </summary>
public new static QueryResult<T> Fail<T>(string exchange, Error error) => new QueryResult<T>(exchange, default, error);
/// <inheritdoc />
public string? RequestBody { get; init; }
}
/// <inheritdoc />
public record QueryResult<T> : QueryResult, IQueryResult<T>
{
/// <summary>
/// ctor
/// </summary>
public QueryResult(string exchange, T? value, Error? error) : base(exchange, error)
{
Data = value;
}
/// <inheritdoc />
public new Error? Error
{
get => base.Error;
init => base.Error = value;
}
/// <inheritdoc />
[MemberNotNullWhen(false, nameof(Error))]
[MemberNotNullWhen(true, nameof(Data))]
public new bool Success => Error == null;
/// <inheritdoc />
public T? Data { get; set; }
/// <inheritdoc />
public string? OriginalData { get; init; }
}
+27 -25
View File
@@ -262,7 +262,7 @@ namespace CryptoExchange.Net.OrderBook
}
/// <inheritdoc/>
public async Task<CallResult<bool>> StartAsync(CancellationToken? ct = null)
public async Task<CallResult> StartAsync(CancellationToken? ct = null)
{
if (Status != OrderBookStatus.Disconnected)
throw new InvalidOperationException($"Can't start book unless state is {OrderBookStatus.Disconnected}. Current state: {Status}");
@@ -286,10 +286,10 @@ namespace CryptoExchange.Net.OrderBook
_processTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
var startResult = await DoStartAsync(_cts.Token).ConfigureAwait(false);
if (!startResult)
if (!startResult.Success)
{
Status = OrderBookStatus.Disconnected;
return new CallResult<bool>(startResult.Error!);
return CallResult.Fail(startResult.Error!);
}
if (_cts.IsCancellationRequested)
@@ -297,7 +297,7 @@ namespace CryptoExchange.Net.OrderBook
_logger.OrderBookStoppedStarting(Api, Symbol);
await startResult.Data.CloseAsync().ConfigureAwait(false);
Status = OrderBookStatus.Disconnected;
return new CallResult<bool>(new CancellationRequestedError());
return CallResult.Fail(new CancellationRequestedError());
}
_subscription = startResult.Data;
@@ -306,7 +306,7 @@ namespace CryptoExchange.Net.OrderBook
_subscription.ConnectionRestored += HandleConnectionRestored;
Status = OrderBookStatus.Synced;
return new CallResult<bool>(true);
return CallResult.Ok();
}
private void HandleConnectionLost()
@@ -354,7 +354,7 @@ namespace CryptoExchange.Net.OrderBook
public CallResult<decimal> CalculateAverageFillPrice(decimal baseQuantity, OrderBookEntryType type)
{
if (Status != OrderBookStatus.Synced)
return new CallResult<decimal>(new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state"));
return CallResult<decimal>.Fail(new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state"));
var totalCost = 0m;
var totalAmount = 0m;
@@ -367,7 +367,7 @@ namespace CryptoExchange.Net.OrderBook
while (amountLeft > 0)
{
if (step == list.Count)
return new CallResult<decimal>(new InvalidOperationError("Quantity is larger than order in the order book"));
return CallResult<decimal>.Fail(new InvalidOperationError("Quantity is larger than order in the order book"));
var element = list.ElementAt(step);
var stepAmount = Math.Min(element.Value.Quantity, amountLeft);
@@ -378,14 +378,14 @@ namespace CryptoExchange.Net.OrderBook
}
}
return new CallResult<decimal>(Math.Round(totalCost / totalAmount, 8));
return CallResult<decimal>.Ok(Math.Round(totalCost / totalAmount, 8));
}
/// <inheritdoc/>
public CallResult<decimal> CalculateTradableAmount(decimal quoteQuantity, OrderBookEntryType type)
{
if (Status != OrderBookStatus.Synced)
return new CallResult<decimal>(new InvalidOperationError($"{nameof(CalculateTradableAmount)} is not available when book is not in Synced state"));
return CallResult<decimal>.Fail(new InvalidOperationError($"{nameof(CalculateTradableAmount)} is not available when book is not in Synced state"));
var quoteQuantityLeft = quoteQuantity;
var totalBaseQuantity = 0m;
@@ -397,7 +397,7 @@ namespace CryptoExchange.Net.OrderBook
while (quoteQuantityLeft > 0)
{
if (step == list.Count)
return new CallResult<decimal>(new InvalidOperationError("Quantity is larger than order in the order book"));
return CallResult<decimal>.Fail(new InvalidOperationError("Quantity is larger than order in the order book"));
var element = list.ElementAt(step);
var stepAmount = Math.Min(element.Value.Quantity * element.Value.Price, quoteQuantityLeft);
@@ -407,7 +407,7 @@ namespace CryptoExchange.Net.OrderBook
}
}
return new CallResult<decimal>(Math.Round(totalBaseQuantity, 8));
return CallResult<decimal>.Ok(Math.Round(totalBaseQuantity, 8));
}
/// <summary>
@@ -426,7 +426,7 @@ namespace CryptoExchange.Net.OrderBook
/// Resync the order book
/// </summary>
/// <returns></returns>
protected abstract Task<CallResult<bool>> DoResyncAsync(CancellationToken ct);
protected abstract Task<CallResult> DoResyncAsync(CancellationToken ct);
/// <summary>
/// Implementation for validating a checksum value with the current order book. If checksum validation fails (returns false)
@@ -605,10 +605,9 @@ namespace CryptoExchange.Net.OrderBook
var listToChange = type == OrderBookEntryType.Ask ? _asks : _bids;
if (entry.Quantity == 0)
{
if (!listToChange.ContainsKey(entry.Price))
if (!listToChange.Remove(entry.Price))
return true;
listToChange.Remove(entry.Price);
if (type == OrderBookEntryType.Ask) AskCount--;
else BidCount--;
}
@@ -635,16 +634,16 @@ namespace CryptoExchange.Net.OrderBook
/// <param name="timeout">Max wait time</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected async Task<CallResult<bool>> WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct)
protected async Task<CallResult> WaitForSetOrderBookAsync(TimeSpan timeout, CancellationToken ct)
{
var startWait = DateTime.UtcNow;
while (!_bookSet && Status == OrderBookStatus.Syncing)
{
if(ct.IsCancellationRequested)
return new CallResult<bool>(new CancellationRequestedError());
return CallResult.Fail(new CancellationRequestedError());
if (DateTime.UtcNow - startWait > timeout)
return new CallResult<bool>(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
return CallResult.Fail(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
try
{
@@ -654,7 +653,7 @@ namespace CryptoExchange.Net.OrderBook
{ }
}
return new CallResult<bool>(true);
return CallResult.Ok();
}
/// <summary>
@@ -670,10 +669,10 @@ namespace CryptoExchange.Net.OrderBook
while (_processBuffer.Count == 0)
{
if (ct.IsCancellationRequested)
return new CallResult(new CancellationRequestedError());
return CallResult.Fail(new CancellationRequestedError());
if (DateTime.UtcNow - startWait > maxWait)
return new CallResult(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
return CallResult.Fail(new ServerError(new ErrorInfo(ErrorType.OrderBookTimeout, "Timeout while waiting for data")));
try
{
@@ -690,7 +689,7 @@ namespace CryptoExchange.Net.OrderBook
await Task.Delay(minWait.Value - dif).ConfigureAwait(false);
}
return CallResult.SuccessResult;
return CallResult.Ok();
}
/// <summary>
@@ -809,7 +808,7 @@ namespace CryptoExchange.Net.OrderBook
return;
var resyncResult = await DoResyncAsync(_cts!.Token).ConfigureAwait(false);
success = resyncResult;
success = resyncResult.Success;
}
_logger.OrderBookResynced(Api, Symbol);
@@ -835,7 +834,7 @@ namespace CryptoExchange.Net.OrderBook
if (item is OrderBookSnapshot snapshot)
ProcessOrderBookSnapshot(snapshot);
if (item is OrderBookUpdate update)
else if (item is OrderBookUpdate update)
ProcessQueueItem(update);
else if (item is OrderBookChecksum checksum)
ProcessChecksum(checksum);
@@ -963,7 +962,8 @@ namespace CryptoExchange.Net.OrderBook
await _subscription!.UnsubscribeAsync().ConfigureAwait(false);
Reset();
_stopProcessing = false;
if (!await _subscription!.ResubscribeAsync().ConfigureAwait(false))
var resubResult = await _subscription!.ResubscribeAsync().ConfigureAwait(false);
if (!resubResult.Success)
{
// Resubscribing failed, reconnect the socket
_logger.OrderBookResyncFailed(Api, Symbol);
@@ -1055,10 +1055,12 @@ namespace CryptoExchange.Net.OrderBook
private SequenceNumberResult ValidateLiveSequenceNumber(long sequenceNumber)
{
if (sequenceNumber < LastSequenceNumber
if (sequenceNumber < LastSequenceNumber
&& (_firstUpdateAfterSnapshotDone || !_skipSequenceCheckFirstUpdateAfterSnapshotSet))
{
// Update is somehow from before the current state
return SequenceNumberResult.OutOfSync;
}
if (_sequencesAreConsecutive
&& LastSequenceNumber != 0
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
=> definition.Authenticated == _authenticated;
}
}
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
=> string.Equals(definition.Path, _path, StringComparison.OrdinalIgnoreCase);
}
}
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
=> _paths.Contains(definition.Path);
}
}
@@ -20,8 +20,8 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
=> host.Equals(_host, System.StringComparison.InvariantCulture);
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
=> definition.BaseAddress.Equals(_host, System.StringComparison.InvariantCulture);
}
}
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
=> type == _type;
}
}
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
}
/// <inheritdoc />
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
public bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey)
=> definition.Path.TrimStart('/').StartsWith(_path, StringComparison.OrdinalIgnoreCase);
}
}
@@ -14,30 +14,30 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <summary>
/// Apply guard per host
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerHost { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => host);
public static Func<RequestDefinition, string?, string> PerHost { get; } = new Func<RequestDefinition, string?, string>((def, key) => def.BaseAddress);
/// <summary>
/// Apply guard per endpoint
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
public static Func<RequestDefinition, string?, string> PerEndpoint { get; } = new Func<RequestDefinition, string?, string>((def, key) => def.Path + def.Method);
/// <summary>
/// Apply guard per connection
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerConnection { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.ConnectionId.ToString()!);
public static Func<RequestDefinition, string?, string> PerConnection { get; } = new Func<RequestDefinition, string?, string>((def, key) => def.ConnectionId.ToString()!);
/// <summary>
/// Apply guard per API key
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key!);
public static Func<RequestDefinition, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string?, string>((def, key) => key!);
/// <summary>
/// Apply guard per API key per endpoint
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key! + def.Path + def.Method);
public static Func<RequestDefinition, string?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string?, string>((def, key) => key! + def.Path + def.Method);
private readonly IEnumerable<IGuardFilter> _filters;
private readonly Dictionary<string, IWindowTracker> _trackers;
private readonly RateLimitWindowType _windowType;
private readonly double? _decayRate;
private readonly int? _connectionWeight;
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
private readonly Func<RequestDefinition, string?, string> _keySelector;
private readonly SemaphoreSlim? _sharedGuardSemaphore;
/// <inheritdoc />
@@ -71,7 +71,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</param>
/// <param name="shared">Whether this guard is shared between multiple gates</param>
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight, shared)
{
}
@@ -87,7 +87,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</param>
/// <param name="shared">Whether this guard is shared between multiple gates</param>
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
{
_filters = filters;
_trackers = new Dictionary<string, IWindowTracker>();
@@ -104,11 +104,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
{
foreach (var filter in _filters)
{
if (!filter.Passes(type, definition, host, apiKey))
if (!filter.Passes(type, definition, apiKey))
return LimitCheck.NotApplicable;
}
@@ -120,7 +120,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
try
{
var key = _keySelector(definition, host, apiKey) + keySuffix;
var key = _keySelector(definition, apiKey) + keySuffix;
if (!_trackers.TryGetValue(key, out var tracker))
{
tracker = CreateTracker();
@@ -141,11 +141,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
{
foreach (var filter in _filters)
{
if (!filter.Passes(type, definition, host, apiKey))
if (!filter.Passes(type, definition, apiKey))
return RateLimitState.NotApplied;
}
@@ -153,7 +153,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
requestWeight = _connectionWeight ?? requestWeight;
var key = _keySelector(definition, host, apiKey) + keySuffix;
var key = _keySelector(definition, apiKey) + keySuffix;
var tracker = _trackers[key];
if (SharedGuard)
@@ -173,11 +173,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix)
public void Reset(RateLimitItemType type, RequestDefinition definition, string? apiKey, string? keySuffix, int? amount)
{
foreach (var filter in _filters)
{
if (!filter.Passes(type, definition, host, apiKey))
if (!filter.Passes(type, definition, apiKey))
return;
}
@@ -186,11 +186,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
try
{
var key = _keySelector(definition, host, apiKey) + keySuffix;
var key = _keySelector(definition, apiKey) + keySuffix;
if (!_trackers.TryGetValue(key, out var tracker))
return;
tracker.Reset();
tracker.Reset(amount);
}
finally
{
@@ -42,7 +42,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
{
if (type != Type)
return LimitCheck.NotApplicable;
@@ -55,7 +55,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
{
return RateLimitState.NotApplied;
}
@@ -67,7 +67,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
public void UpdateAfter(DateTime after) => After = after;
/// <inheritdoc />
public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix)
public void Reset(RateLimitItemType type, RequestDefinition definition, string? apiKey, string? keySuffix, int? amount)
{
After = DateTime.UtcNow;
}
@@ -14,19 +14,19 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <summary>
/// Default endpoint limit
/// </summary>
public static Func<RequestDefinition, string, string?, string> Default { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
public static Func<RequestDefinition, string?, string> Default { get; } = new Func<RequestDefinition, string?, string>((def, key) => def.Path + def.Method);
/// <summary>
/// Endpoint limit per API key
/// </summary>
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method + key);
public static Func<RequestDefinition, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string?, string>((def, key) => def.Path + def.Method + key);
private readonly Dictionary<string, IWindowTracker> _trackers;
private readonly RateLimitWindowType _windowType;
private readonly double? _decayRate;
private readonly int _limit;
private readonly TimeSpan _period;
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
private readonly Func<RequestDefinition, string?, string> _keySelector;
/// <inheritdoc />
public string Name => "EndpointLimitGuard";
@@ -42,7 +42,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
TimeSpan period,
RateLimitWindowType windowType,
double? decayRate = null,
Func<RequestDefinition, string, string?, string>? keySelector = null)
Func<RequestDefinition, string?, string>? keySelector = null)
{
_limit = limit;
_period = period;
@@ -53,9 +53,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
{
var key = _keySelector(definition, host, apiKey) + keySuffix;
var key = _keySelector(definition, apiKey) + keySuffix;
if (!_trackers.TryGetValue(key, out var tracker))
{
tracker = CreateTracker();
@@ -70,9 +70,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix)
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
{
var key = _keySelector(definition, host, apiKey) + keySuffix;
var key = _keySelector(definition, apiKey) + keySuffix;
var tracker = _trackers[key];
tracker.ApplyWeight(requestWeight);
return RateLimitState.Applied(_limit, _period, tracker.Current);
@@ -90,13 +90,13 @@ namespace CryptoExchange.Net.RateLimiting.Guards
}
/// <inheritdoc />
public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix)
public void Reset(RateLimitItemType type, RequestDefinition definition, string? apiKey, string? keySuffix, int? amount)
{
var key = _keySelector(definition, host, apiKey) + keySuffix;
var key = _keySelector(definition, apiKey) + keySuffix;
if (!_trackers.TryGetValue(key, out var tracker))
return;
tracker.Reset();
tracker.Reset(amount);
}
}
}
@@ -12,9 +12,8 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// </summary>
/// <param name="type">The type of item</param>
/// <param name="definition">The request definition</param>
/// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param>
/// <returns>True if passed</returns>
bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey);
bool Passes(RateLimitItemType type, RequestDefinition definition, string? apiKey);
}
}
@@ -49,14 +49,13 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="itemId">Id of the item to check</param>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="baseAddress">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="requestWeight">Request weight</param>
/// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <param name="ct">Cancelation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
ValueTask<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
ValueTask<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
/// <summary>
/// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail
@@ -66,30 +65,29 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="guard">The guard</param>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="baseAddress">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="requestWeight">The weight to apply to the limit guard</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <param name="ct">Cancelation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
ValueTask<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
ValueTask<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
/// <summary>
/// Reset the limit for the specified parameters
/// </summary>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <param name="amount">Amount in weight to reset by, or null to set used rate limit to 0</param>
/// <param name="ct">Cancelation token</param>
Task ResetAsync(
RateLimitItemType type,
RequestDefinition definition,
string host,
string? apiKey,
string? keySuffix,
int? amount,
CancellationToken ct);
}
}
@@ -22,33 +22,31 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// </summary>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="requestWeight">The request weight</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns>
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix);
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix);
/// <summary>
/// Apply the request to this guard with the specified weight
/// </summary>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="requestWeight">The request weight</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns>
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix);
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix);
/// <summary>
/// Reset the limit for the specified parameters
/// </summary>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix);
/// <param name="amount">Amount in weight to reset by, or null to set used rate limit to 0</param>
void Reset(RateLimitItemType type, RequestDefinition definition, string? apiKey, string? keySuffix, int? amount);
}
}
@@ -33,6 +33,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <summary>
/// Reset the limit counter for this tracker
/// </summary>
void Reset();
void Reset(int? amount);
}
}
@@ -25,10 +25,6 @@ namespace CryptoExchange.Net.RateLimiting
/// </summary>
public RequestDefinition RequestDefinition { get; set; }
/// <summary>
/// The host the request is for
/// </summary>
public string Host { get; set; } = default!;
/// <summary>
/// The current counter value
/// </summary>
public int Current { get; set; }
@@ -56,13 +52,12 @@ namespace CryptoExchange.Net.RateLimiting
/// <summary>
/// ctor
/// </summary>
public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
{
ItemId = itemId;
ApiLimit = apiLimit;
LimitDescription = limitDescription;
RequestDefinition = definition;
Host = host;
Current = current;
RequestWeight = requestWeight;
Limit = limit;
@@ -37,20 +37,20 @@ namespace CryptoExchange.Net.RateLimiting
}
/// <inheritdoc />
public async ValueTask<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct)
public async ValueTask<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct)
{
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
bool release = true;
_waitingCount++;
try
{
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
}
catch (TaskCanceledException tce)
{
// The semaphore has already been released if the task was cancelled
release = false;
return new CallResult(new CancellationRequestedError(tce));
return CallResult.Fail(new CancellationRequestedError(tce));
}
finally
{
@@ -67,7 +67,6 @@ namespace CryptoExchange.Net.RateLimiting
IRateLimitGuard guard,
RateLimitItemType type,
RequestDefinition definition,
string host,
string? apiKey,
int requestWeight,
RateLimitingBehaviour rateLimitingBehaviour,
@@ -79,13 +78,13 @@ namespace CryptoExchange.Net.RateLimiting
_waitingCount++;
try
{
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
}
catch (TaskCanceledException tce)
{
// The semaphore has already been released if the task was cancelled
release = false;
return new CallResult(new CancellationRequestedError(tce));
return CallResult.Fail(new CancellationRequestedError(tce));
}
finally
{
@@ -95,12 +94,12 @@ namespace CryptoExchange.Net.RateLimiting
}
}
private async ValueTask<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct)
private async ValueTask<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct)
{
foreach (var guard in guards)
{
// Check if a wait is needed for this guard
var result = guard.Check(type, definition, host, apiKey, requestWeight, keySuffix);
var result = guard.Check(type, definition, apiKey, requestWeight, keySuffix);
if (result.Delay != TimeSpan.Zero && rateLimitingBehaviour == RateLimitingBehaviour.Fail)
{
// Delay is needed and limit behaviour is to fail the request
@@ -109,8 +108,8 @@ namespace CryptoExchange.Net.RateLimiting
else
logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description);
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
return new CallResult(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
return CallResult.Fail(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
}
if (result.Delay != TimeSpan.Zero)
@@ -124,17 +123,17 @@ namespace CryptoExchange.Net.RateLimiting
else
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
return await CheckGuardsAsync(guards, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
}
}
// Apply the weight on each guard
foreach (var guard in guards)
{
var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight, keySuffix);
var result = guard.ApplyWeight(type, definition, apiKey, requestWeight, keySuffix);
if (result.IsApplied)
{
RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period));
@@ -149,7 +148,7 @@ namespace CryptoExchange.Net.RateLimiting
}
}
return CallResult.SuccessResult;
return CallResult.Ok();
}
/// <inheritdoc />
@@ -198,16 +197,16 @@ namespace CryptoExchange.Net.RateLimiting
public async Task ResetAsync(
RateLimitItemType type,
RequestDefinition definition,
string host,
string? apiKey,
string? keySuffix,
int? amount,
CancellationToken ct)
{
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
try
{
foreach (var guard in _guards)
guard.Reset(type, definition, host, apiKey, keySuffix);
guard.Reset(type, definition, apiKey, keySuffix, amount);
}
finally
{
@@ -27,10 +27,17 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
}
/// <inheritdoc />
public void Reset()
public void Reset(int? amount)
{
_currentWeight = 0;
_lastDecrease = DateTime.UtcNow;
if (amount == null)
{
_lastDecrease = DateTime.UtcNow;
_currentWeight = 0;
}
else
{
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
}
}
/// <inheritdoc />
@@ -30,11 +30,27 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
}
/// <inheritdoc />
public void Reset()
public void Reset(int? amount)
{
_entries.Clear();
_currentWeight = 0;
_nextReset = null;
if (amount == null)
{
_entries.Clear();
_currentWeight = 0;
_nextReset = null;
}
else
{
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
var removedWeight = 0;
while (true)
{
if (removedWeight >= amount.Value || _entries.Count == 0)
break;
var lastEntry = _entries.Dequeue();
removedWeight += lastEntry.Weight;
}
}
}
public TimeSpan GetWaitTime(int weight)
@@ -29,10 +29,26 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
}
/// <inheritdoc />
public void Reset()
public void Reset(int? amount)
{
_entries.Clear();
_currentWeight = 0;
if (amount == null)
{
_entries.Clear();
_currentWeight = 0;
}
else
{
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
var removedWeight = 0;
while (true)
{
if (removedWeight >= amount.Value || _entries.Count == 0)
break;
var lastEntry = _entries.Dequeue();
removedWeight += lastEntry.Weight;
}
}
}
/// <inheritdoc />
@@ -29,10 +29,27 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
}
/// <inheritdoc />
public void Reset()
public void Reset(int? amount)
{
_entries.Clear();
_currentWeight = 0;
if (amount == null)
{
_entries.Clear();
_currentWeight = 0;
}
else
{
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
var removedWeight = 0;
while (true)
{
if (removedWeight >= amount.Value || _entries.Count == 0)
break;
var lastEntry = _entries[_entries.Count - 1];
removedWeight += lastEntry.Weight;
_entries.Remove(lastEntry);
}
}
}
/// <inheritdoc />
@@ -22,6 +22,11 @@ namespace CryptoExchange.Net.SharedApis
/// </summary>
bool Authenticated { get; }
/// <summary>
/// Get info on the client and supported features
/// </summary>
SharedClientInfo Discover();
/// <summary>
/// Format a base and quote asset to an exchange accepted symbol
/// </summary>
@@ -33,14 +38,15 @@ namespace CryptoExchange.Net.SharedApis
string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
/// <summary>
/// Set a default exchange parameter. This can be used instead of passing in an ExchangeParameters object which each request.
/// Set a default exchange parameter which will be statically set with each request. This can be used instead of passing it in an ExchangeParameters object with each request.<br />
/// Default exchange parameters can still be overridden by passing the parameter in the ExchangeParameters of a request.
/// </summary>
/// <param name="name">Parameter name</param>
/// <param name="value">Parameter value</param>
void SetDefaultExchangeParameter(string name, object value);
/// <summary>
/// Reset the default exchange parameters, resets parameters for all exchanges
/// Reset previously set default exchange parameters for the exchange.
/// </summary>
void ResetDefaultExchangeParameters();
}
@@ -1,4 +1,5 @@
using System.Threading;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
@@ -9,15 +10,18 @@ namespace CryptoExchange.Net.SharedApis
public interface IFundingRateRestClient : ISharedClient
{
/// <summary>
/// Funding rate request options
/// Funding rate request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
GetFundingRateHistoryOptions GetFundingRateHistoryOptions { get; }
/// <summary>
/// Get funding rate records
/// Get funding rate records, see <see cref="GetFundingRateHistoryOptions"/> for request options and exchange specific required/optional parameters. <br />
/// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination.
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFundingRate[]>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
Task<HttpResult<SharedFundingRate[]>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
}
}
@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using System.Threading;
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.SharedApis
{
@@ -9,26 +10,30 @@ namespace CryptoExchange.Net.SharedApis
public interface IFuturesOrderClientIdRestClient : ISharedClient
{
/// <summary>
/// Futures get order by client order id request options
/// Futures get order by client order id request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<GetOrderRequest> GetFuturesOrderByClientOrderIdOptions { get; }
GetFuturesOrderByClientOrderIdOptions GetFuturesOrderByClientOrderIdOptions { get; }
/// <summary>
/// Get info on a specific futures order using a client order id
/// Get info on a specific futures order using a client order id, see <see cref="GetFuturesOrderByClientOrderIdOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFuturesOrder>> GetFuturesOrderByClientOrderIdAsync(GetOrderRequest request, CancellationToken ct = default);
Task<HttpResult<SharedFuturesOrder>> GetFuturesOrderByClientOrderIdAsync(GetOrderRequest request, CancellationToken ct = default);
/// <summary>
/// Futures cancel order by client order id request options
/// Futures cancel order by client order id request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<CancelOrderRequest> CancelFuturesOrderByClientOrderIdOptions { get; }
CancelFuturesOrderByClientOrderIdOptions CancelFuturesOrderByClientOrderIdOptions { get; }
/// <summary>
/// Cancel a futures order using client order id
/// Cancel a futures order using client order id, see <see cref="CancelFuturesOrderByClientOrderIdOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedId>> CancelFuturesOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default);
Task<HttpResult<SharedId>> CancelFuturesOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default);
}
}
@@ -1,4 +1,5 @@
using System.Threading;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
@@ -16,17 +17,16 @@ namespace CryptoExchange.Net.SharedApis
/// How the asset is determined in which the trading fee is paid
/// </summary>
SharedFeeAssetType FuturesFeeAssetType { get; }
/// <summary>
/// Supported order types
/// Supported order types for futures orders
/// </summary>
SharedOrderType[] FuturesSupportedOrderTypes { get; }
/// <summary>
/// Supported time in force
/// Supported time in force types for futures orders
/// </summary>
SharedTimeInForce[] FuturesSupportedTimeInForce { get; }
/// <summary>
/// Quantity types support
/// Supported quantity types for futures orders
/// </summary>
SharedQuantitySupport FuturesSupportedOrderQuantity { get; }
@@ -37,106 +37,126 @@ namespace CryptoExchange.Net.SharedApis
string GenerateClientOrderId();
/// <summary>
/// Futures place order request options
/// Futures place order request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
PlaceFuturesOrderOptions PlaceFuturesOrderOptions { get; }
/// <summary>
/// Place a new futures order
/// Place a new futures order, see <see cref="PlaceFuturesOrderOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedId>> PlaceFuturesOrderAsync(PlaceFuturesOrderRequest request, CancellationToken ct = default);
Task<HttpResult<SharedId>> PlaceFuturesOrderAsync(PlaceFuturesOrderRequest request, CancellationToken ct = default);
/// <summary>
/// Futures get order request options
/// Futures get order request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<GetOrderRequest> GetFuturesOrderOptions { get; }
GetFuturesOrderOptions GetFuturesOrderOptions { get; }
/// <summary>
/// Get info on a specific futures order
/// Get info on a specific futures order, see <see cref="GetFuturesOrderOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFuturesOrder>> GetFuturesOrderAsync(GetOrderRequest request, CancellationToken ct = default);
Task<HttpResult<SharedFuturesOrder>> GetFuturesOrderAsync(GetOrderRequest request, CancellationToken ct = default);
/// <summary>
/// Futures get open orders request options
/// Futures get open orders request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<GetOpenOrdersRequest> GetOpenFuturesOrdersOptions { get; }
GetOpenFuturesOrdersOptions GetOpenFuturesOrdersOptions { get; }
/// <summary>
/// Get info on a open futures orders
/// Get info on a open futures orders, see <see cref="GetOpenFuturesOrdersOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFuturesOrder[]>> GetOpenFuturesOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default);
Task<HttpResult<SharedFuturesOrder[]>> GetOpenFuturesOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default);
/// <summary>
/// Spot get closed orders request options
/// Spot get closed orders request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
GetClosedOrdersOptions GetClosedFuturesOrdersOptions { get; }
GetFuturesClosedOrdersOptions GetClosedFuturesOrdersOptions { get; }
/// <summary>
/// Get info on closed futures orders
/// Get info on closed futures orders, see <see cref="GetClosedFuturesOrdersOptions"/> for request options and exchange specific required/optional parameters. <br />
/// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination.
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFuturesOrder[]>> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
Task<HttpResult<SharedFuturesOrder[]>> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
/// <summary>
/// Futures get order trades request options
/// Futures get order trades request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<GetOrderTradesRequest> GetFuturesOrderTradesOptions { get; }
GetFuturesOrderTradesOptions GetFuturesOrderTradesOptions { get; }
/// <summary>
/// Get trades for a specific futures order
/// Get trades for a specific futures order, see <see cref="GetFuturesOrderTradesOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedUserTrade[]>> GetFuturesOrderTradesAsync(GetOrderTradesRequest request, CancellationToken ct = default);
Task<HttpResult<SharedUserTrade[]>> GetFuturesOrderTradesAsync(GetOrderTradesRequest request, CancellationToken ct = default);
/// <summary>
/// Futures user trades request options
/// Futures user trades request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
GetUserTradesOptions GetFuturesUserTradesOptions { get; }
GetFuturesUserTradesOptions GetFuturesUserTradesOptions { get; }
/// <summary>
/// Get futures user trade records
/// Get futures user trade records, see <see cref="GetFuturesUserTradesOptions"/> for request options and exchange specific required/optional parameters. <br />
/// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination.
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedUserTrade[]>> GetFuturesUserTradesAsync(GetUserTradesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
Task<HttpResult<SharedUserTrade[]>> GetFuturesUserTradesAsync(GetUserTradesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
/// <summary>
/// Futures cancel order request options
/// Futures cancel order request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<CancelOrderRequest> CancelFuturesOrderOptions { get; }
CancelFuturesOrderOptions CancelFuturesOrderOptions { get; }
/// <summary>
/// Cancel a futures order
/// Cancel a futures order, see <see cref="CancelFuturesOrderOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedId>> CancelFuturesOrderAsync(CancelOrderRequest request, CancellationToken ct = default);
Task<HttpResult<SharedId>> CancelFuturesOrderAsync(CancelOrderRequest request, CancellationToken ct = default);
/// <summary>
/// Positions request options
/// Positions request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<GetPositionsRequest> GetPositionsOptions { get; }
GetPositionsOptions GetPositionsOptions { get; }
/// <summary>
/// Get open position info
/// Get open position info, see <see cref="GetPositionsOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedPosition[]>> GetPositionsAsync(GetPositionsRequest request, CancellationToken ct = default);
Task<HttpResult<SharedPosition[]>> GetPositionsAsync(GetPositionsRequest request, CancellationToken ct = default);
/// <summary>
/// Close position order request options
/// Close position order request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<ClosePositionRequest> ClosePositionOptions { get; }
ClosePositionOptions ClosePositionOptions { get; }
/// <summary>
/// Close a currently open position
/// Close a currently open position, see <see cref="ClosePositionOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<SharedId>> ClosePositionAsync(ClosePositionRequest request, CancellationToken ct = default);
Task<HttpResult<SharedId>> ClosePositionAsync(ClosePositionRequest request, CancellationToken ct = default);
}
}
@@ -1,4 +1,5 @@
using System.Threading;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
@@ -9,33 +10,35 @@ namespace CryptoExchange.Net.SharedApis
public interface IFuturesSymbolRestClient : ISharedClient
{
/// <summary>
/// Futures symbol request options
/// Futures symbol request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<GetSymbolsRequest> GetFuturesSymbolsOptions { get; }
GetFuturesSymbolsOptions GetFuturesSymbolsOptions { get; }
/// <summary>
/// Get all futures symbols for a specific base asset
/// </summary>
/// <param name="baseAsset">Asset, for example `ETH`</param>
Task<ExchangeResult<SharedSymbol[]>> GetFuturesSymbolsForBaseAssetAsync(string baseAsset);
Task<ExchangeCallResult<SharedSymbol[]>> GetFuturesSymbolsForBaseAssetAsync(string baseAsset);
/// <summary>
/// Gets whether the client supports a futures symbol
/// </summary>
/// <param name="symbol">The symbol</param>
Task<ExchangeResult<bool>> SupportsFuturesSymbolAsync(SharedSymbol symbol);
Task<ExchangeCallResult<bool>> SupportsFuturesSymbolAsync(SharedSymbol symbol);
/// <summary>
/// Gets whether the client supports a futures symbol
/// </summary>
/// <param name="symbolName">The symbol name</param>
Task<ExchangeResult<bool>> SupportsFuturesSymbolAsync(string symbolName);
Task<ExchangeCallResult<bool>> SupportsFuturesSymbolAsync(string symbolName);
/// <summary>
/// Get info on all futures symbols supported on the exchange
/// Get info on all futures symbols supported on the exchange, see <see cref="GetFuturesSymbolsOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFuturesSymbol[]>> GetFuturesSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default);
Task<HttpResult<SharedFuturesSymbol[]>> GetFuturesSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default);
}
}
@@ -1,4 +1,5 @@
using System.Threading;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
@@ -9,25 +10,29 @@ namespace CryptoExchange.Net.SharedApis
public interface IFuturesTickerRestClient : ISharedClient
{
/// <summary>
/// Futures get ticker request options
/// Futures get ticker request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
GetTickerOptions GetFuturesTickerOptions { get; }
GetFuturesTickerOptions GetFuturesTickerOptions { get; }
/// <summary>
/// Get ticker info for a specific futures symbol
/// Get ticker info for a specific futures symbol, see <see cref="GetFuturesTickerOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFuturesTicker>> GetFuturesTickerAsync(GetTickerRequest request, CancellationToken ct = default);
Task<HttpResult<SharedFuturesTicker>> GetFuturesTickerAsync(GetTickerRequest request, CancellationToken ct = default);
/// <summary>
/// Futures get tickers request options
/// Futures get tickers request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
GetTickersOptions GetFuturesTickersOptions { get; }
GetFuturesTickersOptions GetFuturesTickersOptions { get; }
/// <summary>
/// Get ticker info for all futures symbols
/// Get ticker info for all futures symbols, see <see cref="GetFuturesTickersOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFuturesTicker[]>> GetFuturesTickersAsync(GetTickersRequest request, CancellationToken ct = default);
Task<HttpResult<SharedFuturesTicker[]>> GetFuturesTickersAsync(GetTickersRequest request, CancellationToken ct = default);
}
}
@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using System.Threading;
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.SharedApis
{
@@ -9,27 +10,31 @@ namespace CryptoExchange.Net.SharedApis
public interface IFuturesTpSlRestClient : ISharedClient
{
/// <summary>
/// Set take profit and/or stop loss options
/// Set take profit and/or stop loss options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<SetTpSlRequest> SetFuturesTpSlOptions { get; }
SetFuturesTpSlOptions SetFuturesTpSlOptions { get; }
/// <summary>
/// Set a take profit and/or stop loss for an open position
/// Set a take profit and/or stop loss for an open position, see <see cref="SetFuturesTpSlOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<SharedId>> SetFuturesTpSlAsync(SetTpSlRequest request, CancellationToken ct = default);
Task<HttpResult<SharedId>> SetFuturesTpSlAsync(SetTpSlRequest request, CancellationToken ct = default);
/// <summary>
/// Cancel a take profit and/or stop loss options
/// Cancel a take profit and/or stop loss options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<CancelTpSlRequest> CancelFuturesTpSlOptions { get; }
CancelFuturesTpSlOptions CancelFuturesTpSlOptions { get; }
/// <summary>
/// Cancel an active take profit and/or stop loss for an open position
/// Cancel an active take profit and/or stop loss for an open position, see <see cref="CancelFuturesTpSlOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<bool>> CancelFuturesTpSlAsync(CancelTpSlRequest request, CancellationToken ct = default);
Task<HttpResult<bool>> CancelFuturesTpSlAsync(CancelTpSlRequest request, CancellationToken ct = default);
}
}
@@ -1,4 +1,5 @@
using System.Threading;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
@@ -9,39 +10,44 @@ namespace CryptoExchange.Net.SharedApis
public interface IFuturesTriggerOrderRestClient : ISharedClient
{
/// <summary>
/// Place spot trigger order options
/// Place spot trigger order options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
PlaceFuturesTriggerOrderOptions PlaceFuturesTriggerOrderOptions { get; }
/// <summary>
/// Place a new trigger order
/// Place a new trigger order, see <see cref="PlaceFuturesTriggerOrderOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<SharedId>> PlaceFuturesTriggerOrderAsync(PlaceFuturesTriggerOrderRequest request, CancellationToken ct = default);
Task<HttpResult<SharedId>> PlaceFuturesTriggerOrderAsync(PlaceFuturesTriggerOrderRequest request, CancellationToken ct = default);
/// <summary>
/// Get trigger order request options
/// Get trigger order request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<GetOrderRequest> GetFuturesTriggerOrderOptions { get; }
GetFuturesTriggerOrderOptions GetFuturesTriggerOrderOptions { get; }
/// <summary>
/// Get info on a specific trigger order
/// Get info on a specific trigger order, see <see cref="GetFuturesTriggerOrderOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFuturesTriggerOrder>> GetFuturesTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default);
Task<HttpResult<SharedFuturesTriggerOrder>> GetFuturesTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default);
/// <summary>
/// Cancel trigger order request options
/// Cancel trigger order request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<CancelOrderRequest> CancelFuturesTriggerOrderOptions { get; }
CancelFuturesTriggerOrderOptions CancelFuturesTriggerOrderOptions { get; }
/// <summary>
/// Cancel a trigger order
/// Cancel a trigger order, see <see cref="CancelFuturesTriggerOrderOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedId>> CancelFuturesTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default);
Task<HttpResult<SharedId>> CancelFuturesTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default);
}
}
@@ -1,4 +1,5 @@
using System.Threading;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
@@ -9,15 +10,18 @@ namespace CryptoExchange.Net.SharedApis
public interface IIndexPriceKlineRestClient : ISharedClient
{
/// <summary>
/// Index price klines request options
/// Index price klines request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
GetKlinesOptions GetIndexPriceKlinesOptions { get; }
GetIndexPriceKlinesOptions GetIndexPriceKlinesOptions { get; }
/// <summary>
/// Get index price kline/candlestick data
/// Get index price kline/candlestick data, see <see cref="GetIndexPriceKlinesOptions"/> for request options and exchange specific required/optional parameters. <br />
/// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination.
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFuturesKline[]>> GetIndexPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
Task<HttpResult<SharedFuturesKline[]>> GetIndexPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
}
}
@@ -1,4 +1,5 @@
using System.Threading;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
@@ -14,26 +15,30 @@ namespace CryptoExchange.Net.SharedApis
SharedLeverageSettingMode LeverageSettingType { get; }
/// <summary>
/// Leverage request options
/// Leverage request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<GetLeverageRequest> GetLeverageOptions { get; }
GetLeverageOptions GetLeverageOptions { get; }
/// <summary>
/// Get the current leverage setting for a symbol
/// Get the current leverage setting for a symbol, see <see cref="GetLeverageOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedLeverage>> GetLeverageAsync(GetLeverageRequest request, CancellationToken ct = default);
Task<HttpResult<SharedLeverage>> GetLeverageAsync(GetLeverageRequest request, CancellationToken ct = default);
/// <summary>
/// Leverage set request options
/// Leverage set request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
SetLeverageOptions SetLeverageOptions { get; }
/// <summary>
/// Set the leverage for a symbol
/// Set the leverage for a symbol, see <see cref="SetLeverageOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedLeverage>> SetLeverageAsync(SetLeverageRequest request, CancellationToken ct = default);
Task<HttpResult<SharedLeverage>> SetLeverageAsync(SetLeverageRequest request, CancellationToken ct = default);
}
}
@@ -1,4 +1,5 @@
using System.Threading;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
@@ -9,15 +10,18 @@ namespace CryptoExchange.Net.SharedApis
public interface IMarkPriceKlineRestClient : ISharedClient
{
/// <summary>
/// Mark price klines request options
/// Mark price klines request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
GetKlinesOptions GetMarkPriceKlinesOptions { get; }
GetMarkPriceKlinesOptions GetMarkPriceKlinesOptions { get; }
/// <summary>
/// Get mark price kline/candlestick data
/// Get mark price kline/candlestick data, see <see cref="GetMarkPriceKlinesOptions"/> for request options and exchange specific required/optional parameters. <br />
/// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination.
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedFuturesKline[]>> GetMarkPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
Task<HttpResult<SharedFuturesKline[]>> GetMarkPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
}
}
@@ -1,4 +1,5 @@
using System.Threading;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
@@ -9,14 +10,16 @@ namespace CryptoExchange.Net.SharedApis
public interface IOpenInterestRestClient : ISharedClient
{
/// <summary>
/// Open interest request options
/// Open interest request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<GetOpenInterestRequest> GetOpenInterestOptions { get; }
GetOpenInterestOptions GetOpenInterestOptions { get; }
/// <summary>
/// Get the open interest for a symbol
/// Get the open interest for a symbol, see <see cref="GetOpenInterestOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedOpenInterest>> GetOpenInterestAsync(GetOpenInterestRequest request, CancellationToken ct = default);
Task<HttpResult<SharedOpenInterest>> GetOpenInterestAsync(GetOpenInterestRequest request, CancellationToken ct = default);
}
}
@@ -1,4 +1,5 @@
using System.Threading;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
@@ -9,15 +10,18 @@ namespace CryptoExchange.Net.SharedApis
public interface IPositionHistoryRestClient : ISharedClient
{
/// <summary>
/// Position history request options
/// Position history request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
GetPositionHistoryOptions GetPositionHistoryOptions { get; }
/// <summary>
/// Get position history
/// Get position history, see <see cref="GetPositionHistoryOptions"/> for request options and exchange specific required/optional parameters. <br />
/// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination.
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedPositionHistory[]>> GetPositionHistoryAsync(GetPositionHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
Task<HttpResult<SharedPositionHistory[]>> GetPositionHistoryAsync(GetPositionHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
}
}
@@ -1,4 +1,5 @@
using System.Threading;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
@@ -14,25 +15,29 @@ namespace CryptoExchange.Net.SharedApis
SharedPositionModeSelection PositionModeSettingType { get; }
/// <summary>
/// Position mode request options
/// Position mode request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
GetPositionModeOptions GetPositionModeOptions { get; }
/// <summary>
/// Get the current position mode setting
/// Get the current position mode setting, see <see cref="GetPositionModeOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedPositionModeResult>> GetPositionModeAsync(GetPositionModeRequest request, CancellationToken ct = default);
Task<HttpResult<SharedPositionModeResult>> GetPositionModeAsync(GetPositionModeRequest request, CancellationToken ct = default);
/// <summary>
/// Position mode set request options
/// Position mode set request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
SetPositionModeOptions SetPositionModeOptions { get; }
/// <summary>
/// Set the position mode to a new value
/// Set the position mode to a new value, see <see cref="SetPositionModeOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedPositionModeResult>> SetPositionModeAsync(SetPositionModeRequest request, CancellationToken ct = default);
Task<HttpResult<SharedPositionModeResult>> SetPositionModeAsync(SetPositionModeRequest request, CancellationToken ct = default);
}
}
@@ -1,4 +1,5 @@
using System.Threading;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
@@ -9,27 +10,31 @@ namespace CryptoExchange.Net.SharedApis
public interface IAssetsRestClient : ISharedClient
{
/// <summary>
/// Asset request options
/// Asset request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<GetAssetRequest> GetAssetOptions { get; }
GetAssetOptions GetAssetOptions { get; }
/// <summary>
/// Get info on a specific asset
/// Get info on a specific asset, see <see cref="GetAssetOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedAsset>> GetAssetAsync(GetAssetRequest request, CancellationToken ct = default);
Task<HttpResult<SharedAsset>> GetAssetAsync(GetAssetRequest request, CancellationToken ct = default);
/// <summary>
/// Assets request options
/// Assets request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<GetAssetsRequest> GetAssetsOptions { get; }
GetAssetsOptions GetAssetsOptions { get; }
/// <summary>
/// Get info on all assets the exchange supports
/// Get info on all assets the exchange supports, see <see cref="GetAssetsOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
Task<ExchangeWebResult<SharedAsset[]>> GetAssetsAsync(GetAssetsRequest request, CancellationToken ct = default);
Task<HttpResult<SharedAsset[]>> GetAssetsAsync(GetAssetsRequest request, CancellationToken ct = default);
}
}
@@ -1,4 +1,5 @@
using System.Threading;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
@@ -9,16 +10,18 @@ namespace CryptoExchange.Net.SharedApis
public interface IBalanceRestClient : ISharedClient
{
/// <summary>
/// Balances request options
/// Balances request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
GetBalancesOptions GetBalancesOptions { get; }
/// <summary>
/// Get balances for the user
/// Get balances for the user, see <see cref="GetBalancesOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<SharedBalance[]>> GetBalancesAsync(GetBalancesRequest request, CancellationToken ct = default);
Task<HttpResult<SharedBalance[]>> GetBalancesAsync(GetBalancesRequest request, CancellationToken ct = default);
}
}
@@ -1,4 +1,5 @@
using System.Threading;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
@@ -9,16 +10,18 @@ namespace CryptoExchange.Net.SharedApis
public interface IBookTickerRestClient : ISharedClient
{
/// <summary>
/// Book ticker request options
/// Book ticker request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<GetBookTickerRequest> GetBookTickerOptions { get; }
GetBookTickerOptions GetBookTickerOptions { get; }
/// <summary>
/// Get the best ask/bid info for a symbol
/// Get the best ask/bid info for a symbol, see <see cref="GetBookTickerOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<SharedBookTicker>> GetBookTickerAsync(GetBookTickerRequest request, CancellationToken ct = default);
Task<HttpResult<SharedBookTicker>> GetBookTickerAsync(GetBookTickerRequest request, CancellationToken ct = default);
}
}
@@ -1,4 +1,5 @@
using System.Threading;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
@@ -9,30 +10,34 @@ namespace CryptoExchange.Net.SharedApis
public interface IDepositRestClient : ISharedClient
{
/// <summary>
/// Deposit addresses request options
/// Deposit addresses request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
EndpointOptions<GetDepositAddressesRequest> GetDepositAddressesOptions { get; }
GetDepositAddressesOptions GetDepositAddressesOptions { get; }
/// <summary>
/// Get deposit addresses for an asset
/// Get deposit addresses for an asset, see <see cref="GetDepositAddressesOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<SharedDepositAddress[]>> GetDepositAddressesAsync(GetDepositAddressesRequest request, CancellationToken ct = default);
Task<HttpResult<SharedDepositAddress[]>> GetDepositAddressesAsync(GetDepositAddressesRequest request, CancellationToken ct = default);
/// <summary>
/// Deposits request options
/// Deposits request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
GetDepositsOptions GetDepositsOptions { get; }
/// <summary>
/// Get deposit records
/// Get deposit records, see <see cref="GetDepositsOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary>
/// <param name="request">Request info</param>
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
Task<ExchangeWebResult<SharedDeposit[]>> GetDepositsAsync(GetDepositsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
Task<HttpResult<SharedDeposit[]>> GetDepositsAsync(GetDepositsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
}
}

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