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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
ClientOptions.RateLimitingBehaviour,
|
||||
rateLimitKeySuffix + ClientOptions.RateLimitGroup,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (!limitResult)
|
||||
if (!limitResult.Success)
|
||||
return limitResult.Error!;
|
||||
}
|
||||
}
|
||||
@@ -364,13 +321,12 @@ namespace CryptoExchange.Net.Clients
|
||||
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);
|
||||
|
||||
@@ -419,11 +371,7 @@ namespace CryptoExchange.Net.Clients
|
||||
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;
|
||||
|
||||
@@ -436,9 +384,11 @@ 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,7 +401,7 @@ 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);
|
||||
@@ -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)
|
||||
@@ -573,43 +523,43 @@ namespace CryptoExchange.Net.Clients
|
||||
// Try deserialization into the expected type
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
@@ -1057,8 +1057,10 @@ namespace CryptoExchange.Net.OrderBook
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+13
-8
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+19
-13
@@ -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
Reference in New Issue
Block a user