1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-12 00:43:03 +00:00

Compare commits

..

3 Commits

Author SHA1 Message Date
JKorf 73764970b0 . 2025-08-25 19:11:50 +02:00
JKorf 9ba29035b2 Merge branch 'master' into feature/code-analyzis 2025-08-25 17:37:46 +02:00
JKorf 4c953e2c87 wip 2025-08-24 21:58:52 +02:00
517 changed files with 25720 additions and 40197 deletions
-71
View File
@@ -1,71 +0,0 @@
---
description: Conventions for cross-exchange code using CryptoExchange.Net SharedApis abstractions. Apply when generating C# code that interacts with multiple cryptocurrency exchanges through a unified interface.
globs:
- "**/*.cs"
- "**/*.csproj"
alwaysApply: false
---
# CryptoExchange.Net Conventions
This codebase uses **CryptoExchange.Net** abstractions for multi-exchange access. Each exchange has its own library (Binance.Net, Bybit.Net, OKX.Net, ...). Use `CryptoExchange.Net.SharedApis` for code that should work across exchanges.
## Multi-exchange pattern
```csharp
using Binance.Net.Clients;
using OKX.Net.Clients;
using CryptoExchange.Net.SharedApis;
ISpotTickerRestClient binance = new BinanceRestClient().SpotApi.SharedClient;
ISpotTickerRestClient okx = new OKXRestClient().UnifiedApi.SharedClient;
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
// ticker.Data.LastPrice — same model regardless of exchange
```
## Symbol normalization
`SharedSymbol(TradingMode.Spot, "BTC", "USDT")` is portable. Each library translates to its native format internally. Don't pass raw strings like `"BTCUSDT"` to shared methods.
## Result pattern
Same `WebCallResult<T>` / `CallResult<T>` everywhere. Always check `.Success`. `.Exchange` property identifies which exchange responded — useful for logging.
## Available shared interfaces
- REST tickers/symbols/orderbook/klines/trades, orders (spot/futures, regular/trigger/TP-SL), balances, positions, fees, deposits/withdrawals, transfers
- WebSocket tickers, book tickers, order book, trades, klines, user data
Each exchange documents which it implements. Not every exchange supports every operation.
## Multi-exchange aggregation
Run requests across exchanges concurrently via `Task.WhenAll` — the library is async-safe and concurrent requests are the norm.
```csharp
var clients = new ISpotTickerRestClient[] { binance, okx, bybit };
var tasks = clients.Select(c => c.GetSpotTickerAsync(new GetTickerRequest(symbol)));
var results = await Task.WhenAll(tasks);
```
## Hard rules
- ❌ Never install `CryptoExchange.Net` alone and expect to call exchanges — it's a base library; you need exchange-specific packages
- ❌ Never mix exchange-specific models in cross-exchange code (use `SharedSymbol`, `SharedSpotTicker`, etc.)
- ❌ Never use `.Result` / `.Wait()` — async-only
- ❌ Never iterate sequentially when concurrency is fine — `Task.WhenAll` is your friend
- ❌ Never instantiate clients per-request — reuse via DI
- ✅ Always use `.SharedClient` for cross-exchange code
- ✅ Always check `.Success` before reading `.Data`
- ✅ Always log with `.Exchange` so multi-exchange logs are decipherable
- ✅ Always handle "not supported on this exchange" errors gracefully
## Reference
- `AGENTS.md` in repo root has fuller examples
- `llms.txt` for AI context
- `Examples/ai-friendly/` for compilable examples
- For single-exchange code, see that exchange's library (e.g., Binance.Net `AGENTS.md`)
-52
View File
@@ -1,52 +0,0 @@
# Copilot Instructions for CryptoExchange.Net
This repository is **CryptoExchange.Net** — the base library powering 28+ cryptocurrency exchange wrappers in C#/.NET (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.).
When generating code in this ecosystem, follow these conventions:
## You don't install CryptoExchange.Net directly
Install the exchange-specific library you need (`Binance.Net`, `JK.OKX.Net`, `Bybit.Net`, ...) or `CryptoClients.Net` for the bundle. CryptoExchange.Net is pulled in as a dependency.
## Multi-exchange code uses SharedApis
For code that must work against multiple exchanges, use `CryptoExchange.Net.SharedApis` interfaces accessed via `.SharedClient` properties on each exchange's API surface:
```csharp
using CryptoExchange.Net.SharedApis;
ISpotTickerRestClient binance = new BinanceRestClient().SpotApi.SharedClient;
ISpotTickerRestClient okx = new OKXRestClient().UnifiedApi.SharedClient;
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
```
Same code works on every exchange that implements the interface. Use `Task.WhenAll` for concurrent multi-exchange calls.
## Single-exchange code uses the exchange's own client
For Binance-only code, use `BinanceRestClient` directly (see Binance.Net repo `AGENTS.md`). SharedApis is for portability — use it when you need that.
## 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.
## Available shared interfaces
REST: tickers, symbols, orderbook, klines, trades, orders (spot/futures, trigger, TP-SL), balances, positions, fees, deposits/withdrawals, transfers.
WebSocket: tickers, book tickers, orderbook, trades, klines, user data.
Each exchange library implements a subset. Check exchange docs for support matrix.
## Avoid
- Installing `CryptoExchange.Net` alone and trying to call exchange APIs (need exchange-specific packages)
- Mixing exchange-native models in cross-exchange code (use Shared* types)
- Synchronous `.Result` / `.Wait()` (use `await`)
- Instantiating clients per-request (use DI, reuse instances)
- Sequential per-exchange calls when parallel is fine (`Task.WhenAll`)
## Reference
For detailed patterns see `AGENTS.md` and `llms.txt` in repo root, `examples/ai-friendly/` for compilable examples.
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 10.0.x
dotnet-version: 9.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
-169
View File
@@ -1,169 +0,0 @@
---
name: cryptoexchange-net
description: Use CryptoExchange.Net abstractions when generating C#/.NET code that needs to work across MULTIPLE cryptocurrency exchanges (Binance + Bybit + OKX + Kraken + Coinbase + ...) — for arbitrage, best-execution routing, multi-exchange portfolio dashboards, exchange-agnostic trading bots, or comparison tools. Triggers on requests mentioning multi-exchange, cross-exchange, arbitrage, exchange-agnostic, or unified crypto API in C#. Also triggers when the user is implementing a new exchange library following the CryptoExchange.Net pattern.
---
# CryptoExchange.Net Skill
## When to use
CryptoExchange.Net is the **base library** powering 28+ exchange-specific libraries (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.). You don't install it directly — you install the exchange libraries, which depend on it.
**Three usage modes:**
1. **You target ONE exchange** → use that exchange's library directly (e.g., Binance.Net), see its CLAUDE.md.
2. **You target MULTIPLE exchanges** → install each library you need + use `CryptoExchange.Net.SharedApis` interfaces — write code once, runs against any exchange. **This is the main use case for this skill.**
3. **You want ALL exchanges in one package** → install `CryptoClients.Net`, get `ExchangeRestClient` and `ExchangeSocketClient` with everything bundled.
## Installation
For a multi-exchange project:
```bash
dotnet add package Binance.Net
dotnet add package JK.OKX.Net
dotnet add package Bybit.Net
# ... etc
```
Or the bundle:
```bash
dotnet add package CryptoClients.Net
```
## Core Pattern: Shared Interfaces
Every exchange library exposes `.SharedClient` properties on its API surfaces. These implement the same interfaces from `CryptoExchange.Net.SharedApis`.
```csharp
using Binance.Net.Clients;
using OKX.Net.Clients;
using Bybit.Net.Clients;
using CryptoExchange.Net.SharedApis;
// All three implement ISpotTickerRestClient
ISpotTickerRestClient binance = new BinanceRestClient().SpotApi.SharedClient;
ISpotTickerRestClient okx = new OKXRestClient().UnifiedApi.SharedClient;
ISpotTickerRestClient bybit = new BybitRestClient().V5Api.SharedClient;
// Single agnostic call — works against any of them
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
// ticker.Data.LastPrice, ticker.Data.HighPrice, etc. — same model regardless of exchange
```
## Core Pattern: SharedSymbol
Different exchanges format symbols differently — Binance uses `BTCUSDT`, OKX uses `BTC-USDT`, others may have other formats. `SharedSymbol` normalizes this:
```csharp
var btcusdt = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// Each exchange library translates SharedSymbol → its native format internally.
// For futures:
var btcusdtPerp = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
```
For exchanges that use exotic asset names, see the AssetAliases configuration.
## Available Shared Interfaces
**REST:**
- Market data: `ISpotTickerRestClient`, `IBookTickerRestClient`, `ISpotSymbolRestClient`, `IFuturesSymbolRestClient`, `IOrderBookRestClient`, `IRecentTradeRestClient`, `IKlineRestClient`
- Orders: `ISpotOrderRestClient`, `IFuturesOrderRestClient`, `ISpotOrderClientIdRestClient`, `IFuturesOrderClientIdRestClient`, `ISpotTriggerOrderRestClient`, `IFuturesTriggerOrderRestClient`, `IFuturesTpSlRestClient`
- Account: `IBalanceRestClient`, `IPositionRestClient`, `IFeeRestClient`, `ITransferRestClient`, `IDepositRestClient`, `IWithdrawalRestClient`
**WebSocket:**
- `ITickerSocketClient`, `IBookTickerSocketClient`
- `IOrderBookSocketClient`, `ITradeSocketClient`, `IKlineSocketClient`
- `IUserTradeSocketClient`, `ISpotOrderSocketClient`, `IFuturesOrderSocketClient`, `IPositionSocketClient`, `IBalanceSocketClient`
Each exchange documents which interfaces it implements (some exchanges don't support every operation).
## Core Pattern: Result Handling
Same as exchange-specific libraries — `WebCallResult<T>` (REST) or `CallResult<T>` (WebSocket) with `.Success`, `.Data`, `.Error`. Always check `.Success` first.
```csharp
var result = await sharedClient.GetSpotTickerAsync(new GetTickerRequest(symbol));
if (!result.Success)
{
Console.WriteLine($"[{sharedClient.Exchange}] Error: {result.Error}");
return;
}
Console.WriteLine($"[{sharedClient.Exchange}] {result.Data.LastPrice}");
```
`.Exchange` property on every shared client tells you which exchange you're talking to — useful for logging.
## Core Pattern: Multi-Exchange Aggregation
```csharp
var clients = new ISpotTickerRestClient[]
{
new BinanceRestClient().SpotApi.SharedClient,
new OKXRestClient().UnifiedApi.SharedClient,
new BybitRestClient().V5Api.SharedClient,
};
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// Fetch concurrently from all exchanges
var tasks = clients.Select(c => c.GetSpotTickerAsync(new GetTickerRequest(symbol))).ToArray();
var results = await Task.WhenAll(tasks);
for (int i = 0; i < clients.Length; i++)
{
if (results[i].Success)
Console.WriteLine($"{clients[i].Exchange}: {results[i].Data!.LastPrice}");
}
```
## Per-Exchange Setup
Each exchange library has its own credentials class and options. See each library's CLAUDE.md for specifics. The pattern is consistent: `XxxRestClient(options => { options.ApiCredentials = new XxxCredentials(...); })`.
## Dependency Injection
Each exchange library has its own `services.AddXxx(...)` extension. They all share the same option-builder pattern. Register only the ones you use:
```csharp
services.AddBinance(restOpts => { /*...*/ }, socketOpts => { /*...*/ });
services.AddOKX(restOpts => { /*...*/ }, socketOpts => { /*...*/ });
// Inject IBinanceRestClient, IOKXRestClient, etc.
```
For one-package access: `services.AddCryptoClients(...)` from `CryptoClients.Net`.
## Common Pitfalls — AVOID
- **Do NOT install `CryptoExchange.Net` and try to call exchange APIs directly** — it's a base abstraction; you need an exchange library.
- **Do NOT try to use one exchange's models with another's client** — use the SharedApis types (`SharedSymbol`, `SharedSpotTicker`, `SharedSpotOrder`, etc.) for cross-exchange code.
- **Do NOT block on async operations** — use `await` throughout. `Task.WhenAll` for parallelism across exchanges.
- **Do NOT assume every exchange supports every operation** — check exchange docs or the library's implementation. Operations may return errors like "not supported on this exchange".
- **Do NOT instantiate clients per-request** — reuse via DI.
- **Do NOT iterate exchanges sequentially when concurrency is fine** — use `Task.WhenAll` for ~Nx speedup.
## Implementing a New Exchange Library
If you're building a NEW exchange wrapper following the CryptoExchange.Net pattern (rare but valuable):
- Inherit from `RestApiClient` and `SocketApiClient` base classes
- Define your own `XxxCredentials` extending `ApiCredentials` (or use `ApiCredentials` directly)
- Implement `AuthenticationProvider` for the exchange's signing scheme
- Implement the relevant `Shared*` interfaces on your API client classes for cross-exchange support
- Follow the same `XxxRestOptions` / `XxxSocketOptions` pattern
See existing libraries (Binance.Net, Bybit.Net) as reference implementations.
## Reference
- Source: https://github.com/JKorf/CryptoExchange.Net
- Documentation: https://cryptoexchange.jkorf.dev/
- SharedApis docs: https://cryptoexchange.jkorf.dev/CryptoExchange.Net/idocs_shared.html
- Bundle (all exchanges): https://github.com/JKorf/CryptoClients.Net
- Demo app: https://github.com/JKorf/CryptoManager.Net
- Discord: https://discord.gg/MSpeEtSY8t
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
<Authors>JKorf</Authors>
<Description>Protobuf support for CryptoExchange.Net</Description>
<PackageVersion>10.0.1</PackageVersion>
<AssemblyVersion>10.0.1</AssemblyVersion>
<FileVersion>10.0.1</FileVersion>
<PackageVersion>9.6.0</PackageVersion>
<AssemblyVersion>9.6.0</AssemblyVersion>
<FileVersion>9.6.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType>
@@ -41,7 +41,7 @@
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CryptoExchange.Net" Version="10.0.2" />
<PackageReference Include="CryptoExchange.Net" Version="9.6.0" />
<PackageReference Include="protobuf-net" Version="3.2.56" />
</ItemGroup>
</Project>
-30
View File
@@ -5,36 +5,6 @@
Protobuf support for CryptoExchange.Net.
## Release notes
* Version 10.0.1 - 16 Dec 2025
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 10.0.0 - 16 Dec 2025
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.13.0 - 10 Nov 2025
* Updated CryptoExchange.Net version to 9.13.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.12.0 - 03 Nov 2025
* Updated CryptoExchange.Net version to 9.12.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.11.1 - 30 Oct 2025
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.11.0 - 30 Oct 2025
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.10.0 - 15 Oct 2025
* Updated CryptoExchange.Net version to 9.10.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.9.0 - 06 Oct 2025
* Updated CryptoExchange.Net version to 9.9.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.8.0 - 30 Sep 2025
* Updated CryptoExchange.Net version to 9.8.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.7.0 - 01 Sep 2025
* Updated CryptoExchange.Net version to 9.7.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
* Version 9.6.0 - 25 Aug 2025
* Updated CryptoExchange.Net version to 9.6.0
@@ -4,7 +4,7 @@ using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
@@ -96,7 +96,7 @@ namespace CryptoExchange.Net.UnitTests
waiters.Add(evnt.WaitAsync());
}
List<bool>? results = null;
List<bool> results = null;
var resultsWaiter = Task.Run(async () =>
{
await Task.WhenAll(waiters);
@@ -112,7 +112,7 @@ namespace CryptoExchange.Net.UnitTests
await resultsWaiter;
Assert.That(10 == results?.Count(r => r));
Assert.That(10 == results.Count(r => r));
}
[Test]
@@ -140,17 +140,5 @@ namespace CryptoExchange.Net.UnitTests
ClassicAssert.False(result1);
}
[Test]
public async Task CancellingWait_Should_ReturnFalse()
{
var evnt = new AsyncResetEvent(false, true);
var waiter1 = evnt.WaitAsync(ct: new CancellationTokenSource(50).Token);
var result1 = await waiter1;
ClassicAssert.False(result1);
}
}
}
@@ -1,22 +1,43 @@
using NUnit.Framework;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.UnitTests.ClientTests
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class BaseClientTests
{
//[TestCase]
//public void DeserializingValidJson_Should_GiveSuccessfulResult()
//{
// // arrange
// var client = new TestBaseClient();
[TestCase]
public void DeserializingValidJson_Should_GiveSuccessfulResult()
{
// arrange
var client = new TestBaseClient();
// // act
// var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
// act
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
// // assert
// Assert.That(result.Success);
//}
// assert
Assert.That(result.Success);
}
[TestCase]
public void DeserializingInvalidJson_Should_GiveErrorResult()
{
// arrange
var client = new TestBaseClient();
// act
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
[TestCase("https://api.test.com/api", new[] { "path1", "/path2" }, "https://api.test.com/api/path1/path2")]
@@ -1,43 +0,0 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Text;
using CryptoExchange.Net;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Converters.SystemTextJson;
namespace CryptoExchange.Net.UnitTests
{
internal class BodySerializationTests
{
[Test]
public void ToFormData_SerializesBasicValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", "1" },
{ "b", 2 },
{ "c", true }
};
var parameterString = parameters.ToFormData();
Assert.That(parameterString, Is.EqualTo("a=1&b=2&c=True"));
}
[Test]
public void JsonSerializer_SerializesBasicValuesCorrectly()
{
var serializer = new SystemTextJsonMessageSerializer(SerializerOptions.WithConverters(new TestSerializerContext()));
var parameters = new Dictionary<string, object>()
{
{ "a", "1" },
{ "b", 2 },
{ "c", true }
};
var parameterString = serializer.Serialize(parameters);
Assert.That(parameterString, Is.EqualTo("{\"a\":\"1\",\"b\":2,\"c\":true}"));
}
}
}
+14 -13
View File
@@ -3,8 +3,11 @@ using CryptoExchange.Net.Objects.Errors;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System;
using System.Net;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
@@ -16,7 +19,7 @@ namespace CryptoExchange.Net.UnitTests
{
var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown));
ClassicAssert.AreSame(result.Error!.ErrorCode, "TestError");
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success);
}
@@ -36,7 +39,7 @@ namespace CryptoExchange.Net.UnitTests
{
var result = new CallResult<object>(new ServerError("TestError", ErrorInfo.Unknown));
ClassicAssert.AreSame(result.Error!.ErrorCode, "TestError");
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
ClassicAssert.IsNull(result.Data);
ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success);
@@ -73,7 +76,7 @@ namespace CryptoExchange.Net.UnitTests
var asResult = result.As<TestObject2>(default);
ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError");
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError");
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
@@ -86,7 +89,7 @@ namespace CryptoExchange.Net.UnitTests
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError2");
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2");
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
@@ -99,7 +102,7 @@ namespace CryptoExchange.Net.UnitTests
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError2");
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2");
ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success);
@@ -110,8 +113,7 @@ namespace CryptoExchange.Net.UnitTests
{
var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK,
HttpVersion.Version11,
new HttpResponseMessage().Headers,
new KeyValuePair<string, string[]>[0],
TimeSpan.FromSeconds(1),
null,
"{}",
@@ -119,14 +121,14 @@ namespace CryptoExchange.Net.UnitTests
"https://test.com/api",
null,
HttpMethod.Get,
new HttpRequestMessage().Headers,
new KeyValuePair<string, string[]>[0],
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.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");
@@ -141,8 +143,7 @@ namespace CryptoExchange.Net.UnitTests
{
var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK,
HttpVersion.Version11,
new HttpResponseMessage().Headers,
new KeyValuePair<string, string[]>[0],
TimeSpan.FromSeconds(1),
null,
"{}",
@@ -150,7 +151,7 @@ namespace CryptoExchange.Net.UnitTests
"https://test.com/api",
null,
HttpMethod.Get,
new HttpRequestMessage().Headers,
new KeyValuePair<string, string[]>[0],
ResultDataSource.Server,
new TestObjectResult(),
null);
@@ -1,151 +0,0 @@
using CryptoExchange.Net.Objects;
using NUnit.Framework;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Threading;
using NUnit.Framework.Legacy;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Filters;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Text.Json;
using CryptoExchange.Net.UnitTests.Implementations;
using CryptoExchange.Net.Testing;
namespace CryptoExchange.Net.UnitTests.ClientTests
{
[TestFixture()]
public class RestClientTests
{
[TestCase]
public async Task RequestingData_Should_ResultInData()
{
// arrange
var client = new TestRestClient();
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
var strData = JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() });
client.ApiClient1.SetNextResponse(strData, System.Net.HttpStatusCode.OK);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
Assert.That(result.Success);
Assert.That(TestHelpers.AreEqual(expected, result.Data));
}
[TestCase]
public async Task ReceivingInvalidData_Should_ResultInError()
{
// arrange
var client = new TestRestClient();
client.ApiClient1.SetNextResponse("{\"property\": 123", System.Net.HttpStatusCode.OK);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase]
public async Task ReceivingErrorCode_Should_ResultInError()
{
// arrange
var client = new TestRestClient();
client.ApiClient1.SetNextResponse("Invalid request", System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase]
public async Task ReceivingErrorAndNotParsingError_Should_ResultInFlatError()
{
// arrange
var client = new TestRestClient();
client.ApiClient1.SetNextResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
}
[TestCase]
public async Task ReceivingErrorAndNotParsingErrorAndInvalidJson_Should_ContainData()
{
// arrange
var client = new TestRestClient();
var response = "<html>...</html>";
client.ApiClient1.SetNextResponse(response, System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is DeserializeError);
Assert.That(result.Error!.Message!.Contains(response));
}
[TestCase]
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
{
// arrange
var client = new TestRestClient();
client.ApiClient1.SetNextResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
Assert.That(result.Error!.ErrorCode == "123");
Assert.That(result.Error.Message == "Invalid request");
}
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
[TestCase("POST", HttpMethodParameterPosition.InBody)]
[TestCase("POST", HttpMethodParameterPosition.InUri)]
[TestCase("DELETE", HttpMethodParameterPosition.InBody)]
[TestCase("DELETE", HttpMethodParameterPosition.InUri)]
[TestCase("PUT", HttpMethodParameterPosition.InUri)]
[TestCase("PUT", HttpMethodParameterPosition.InBody)]
public async Task Setting_Should_ResultInOptionsSet(string method, HttpMethodParameterPosition pos)
{
// arrange
// act
var client = new TestRestClient();
var httpMethod = new HttpMethod(method);
client.ApiClient1.SetParameterPosition(httpMethod, pos);
client.ApiClient1.SetNextResponse("{}", System.Net.HttpStatusCode.OK);
var result = await client.ApiClient1.GetResponseAsync<TestObject>(httpMethod, new ParameterCollection
{
{ "TestParam1", "Value1" },
{ "TestParam2", 2 },
});
// assert
Assert.That(result.RequestMethod == new HttpMethod(method));
Assert.That(result.RequestBody?.Contains("TestParam1") == true == (pos == HttpMethodParameterPosition.InBody));
Assert.That((result.RequestUrl?.ToString().Contains("TestParam1")) == (pos == HttpMethodParameterPosition.InUri));
Assert.That(result.RequestBody?.Contains("TestParam2") == true == (pos == HttpMethodParameterPosition.InBody));
Assert.That((result.RequestUrl?.ToString().Contains("TestParam2")) == (pos == HttpMethodParameterPosition.InUri));
}
}
}
@@ -1,177 +0,0 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Testing;
using CryptoExchange.Net.UnitTests.Implementations;
using NUnit.Framework;
using System;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.ClientTests
{
[TestFixture]
public class SocketClientTests
{
[TestCase]
public void SettingOptions_Should_ResultInOptionsSet()
{
//arrange
//act
var client = new TestSocketClient(options =>
{
options.ExchangeOptions.MaxSocketConnections = 1;
});
//assert
Assert.That(1 == client.ApiClient1.ApiOptions.MaxSocketConnections);
}
[TestCase(true)]
[TestCase(false)]
public async Task ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
{
//arrange
var client = new TestSocketClient();
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
socket.CanConnect = canConnect;
//act
var connectResult = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
//assert
Assert.That(connectResult.Success == canConnect);
}
[TestCase]
public async Task SocketMessages_Should_BeProcessedInDataHandlers()
{
var client = new TestSocketClient();
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
var strData = JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() });
TestObject? received = null;
var resetEvent = new AsyncResetEvent(false);
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x =>
{
received = x.Data;
resetEvent.Set();
}, false, default);
socket.InvokeMessage(strData);
await resetEvent.WaitAsync(TimeSpan.FromSeconds(1));
Assert.That(received != null);
}
[TestCase(false)]
[TestCase(true)]
public async Task SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
{
// arrange
var client = new TestSocketClient(options =>
{
options.ReconnectInterval = TimeSpan.Zero;
options.ExchangeOptions.OutputOriginalData = enabled;
});
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
var strData = JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() });
string? originalData = null;
var resetEvent = new AsyncResetEvent(false);
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x =>
{
originalData = x.OriginalData;
resetEvent.Set();
}, false, default);
socket.InvokeMessage(strData);
await resetEvent.WaitAsync(TimeSpan.FromSeconds(1));
// assert
Assert.That(originalData == (enabled ? strData : null));
}
[TestCase()]
public async Task UnsubscribingStream_Should_CloseTheSocket()
{
// arrange
var client = new TestSocketClient(options =>
{
options.ReconnectInterval = TimeSpan.Zero;
});
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var result = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => {}, false, default);
// act
await client.UnsubscribeAsync(result.Data);
// assert
Assert.That(socket.Connected == false);
}
[TestCase()]
public async Task UnsubscribingAll_Should_CloseAllSockets()
{
// arrange
var client = new TestSocketClient(options =>
{
options.ReconnectInterval = TimeSpan.Zero;
});
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var result = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
var socket2 = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var result2 = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
// act
await client.UnsubscribeAllAsync();
// assert
Assert.That(socket.Connected == false);
Assert.That(socket2.Connected == false);
}
[TestCase()]
public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
{
// arrange
var client = new TestSocketClient(opt =>
{
opt.OutputOriginalData = true;
});
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var subTask = client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, true, default);
socket.InvokeMessage(JsonSerializer.Serialize(new TestSocketMessage { Id = 1, Data = "ErrorWithSub" }));
var result = await subTask;
// assert
Assert.That(result.Success == false);
Assert.That(result.Error!.Message!.Contains("ErrorWithSub"));
}
[TestCase()]
public async Task SuccessResponse_Should_ConfirmSubscription()
{
var client = new TestSocketClient();
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var subTask = client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, true, default);
socket.InvokeMessage(JsonSerializer.Serialize(new TestSocketMessage { Id = 1, Data = "OK" }));
var result = await subTask;
var subscription = client.ApiClient1._socketConnections.Single().Value.Subscriptions.Single();
Assert.That(subscription.Status == SubscriptionStatus.Subscribed);
}
}
}
@@ -1,109 +0,0 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Converters.SystemTextJson;
using NUnit.Framework;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
public class ArrayConverterTests
{
[Test()]
public void TestArrayConverter()
{
var data = new Test()
{
Prop1 = 2,
Prop2 = null,
Prop3 = "123",
Prop3Again = "123",
Prop4 = null,
Prop5 = new Test2
{
Prop21 = 3,
Prop22 = "456"
},
Prop6 = new Test3
{
Prop31 = 4,
Prop32 = "789"
},
Prop7 = TestEnum.Two,
TestInternal = new Test
{
Prop1 = 10
},
Prop8 = new Test3
{
Prop31 = 5,
Prop32 = "101"
},
};
var options = new JsonSerializerOptions()
{
TypeInfoResolver = new TestSerializerContext()
};
var serialized = JsonSerializer.Serialize(data);
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
Assert.That(deserialized!.Prop1, Is.EqualTo(2));
Assert.That(deserialized.Prop2, Is.Null);
Assert.That(deserialized.Prop3, Is.EqualTo("123"));
Assert.That(deserialized.Prop3Again, Is.EqualTo("123"));
Assert.That(deserialized.Prop4, Is.Null);
Assert.That(deserialized.Prop5!.Prop21, Is.EqualTo(3));
Assert.That(deserialized.Prop5!.Prop22, Is.EqualTo("456"));
Assert.That(deserialized.Prop6!.Prop31, Is.EqualTo(4));
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
Assert.That(deserialized.TestInternal!.Prop1, Is.EqualTo(10));
Assert.That(deserialized.Prop8!.Prop31, Is.EqualTo(5));
Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101"));
}
}
[JsonConverter(typeof(ArrayConverter<Test>))]
public record Test
{
[ArrayProperty(0)]
public int Prop1 { get; set; }
[ArrayProperty(1)]
public int? Prop2 { get; set; }
[ArrayProperty(2)]
public string? Prop3 { get; set; }
[ArrayProperty(2)]
public string? Prop3Again { get; set; }
[ArrayProperty(3)]
public string? Prop4 { get; set; }
[ArrayProperty(4)]
public Test2? Prop5 { get; set; }
[ArrayProperty(5)]
public Test3? Prop6 { get; set; }
[ArrayProperty(6), JsonConverter(typeof(EnumConverter<TestEnum>))]
public TestEnum? Prop7 { get; set; }
[ArrayProperty(7)]
public Test? TestInternal { get; set; }
[ArrayProperty(8), JsonConversion]
public Test3? Prop8 { get; set; }
}
[JsonConverter(typeof(ArrayConverter<Test2>))]
public record Test2
{
[ArrayProperty(0)]
public int Prop21 { get; set; }
[ArrayProperty(1)]
public string? Prop22 { get; set; }
}
public record Test3
{
[JsonPropertyName("prop31")]
public int Prop31 { get; set; }
[JsonPropertyName("prop32")]
public string? Prop32 { get; set; }
}
}
@@ -1,57 +0,0 @@
using CryptoExchange.Net.Converters.SystemTextJson;
using NUnit.Framework;
using System.Text.Json;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
public class BoolConverterTests
{
[TestCase("1", true)]
[TestCase("true", true)]
[TestCase("yes", true)]
[TestCase("y", true)]
[TestCase("on", true)]
[TestCase("-1", false)]
[TestCase("0", false)]
[TestCase("n", false)]
[TestCase("no", false)]
[TestCase("false", false)]
[TestCase("off", false)]
[TestCase("", null)]
public void TestBoolConverter(string value, bool? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new TestSerializerContext()));
Assert.That(output!.Value == expected);
}
[TestCase("1", true)]
[TestCase("true", true)]
[TestCase("yes", true)]
[TestCase("y", true)]
[TestCase("on", true)]
[TestCase("-1", false)]
[TestCase("0", false)]
[TestCase("n", false)]
[TestCase("no", false)]
[TestCase("false", false)]
[TestCase("off", false)]
[TestCase("", false)]
public void TestBoolConverterNotNullable(string value, bool expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new TestSerializerContext()));
Assert.That(output!.Value == expected);
}
}
public class STJBoolObject
{
public bool? Value { get; set; }
}
public class NotNullableSTJBoolObject
{
public bool Value { get; set; }
}
}
@@ -1,119 +0,0 @@
using CryptoExchange.Net.Converters.SystemTextJson;
using NUnit.Framework;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
public class DateTimeConverterTests
{
[TestCase("2021-05-12")]
[TestCase("20210512")]
[TestCase("210512")]
[TestCase("1620777600.000")]
[TestCase("1620777600000")]
[TestCase("2021-05-12T00:00:00.000Z")]
[TestCase("2021-05-12T00:00:00.000000000Z")]
[TestCase("0.000000", true)]
[TestCase("0", true)]
[TestCase("", true)]
[TestCase(" ", true)]
public void TestDateTimeConverterString(string input, bool expectNull = false)
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": \"{input}\" }}");
Assert.That(output!.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
}
[TestCase(1620777600.000)]
[TestCase(1620777600000d)]
public void TestDateTimeConverterDouble(double input)
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
Assert.That(output!.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[TestCase(1620777600)]
[TestCase(1620777600000)]
[TestCase(1620777600000000)]
[TestCase(1620777600000000000)]
[TestCase(0, true)]
public void TestDateTimeConverterLong(long input, bool expectNull = false)
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
Assert.That(output!.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
}
[TestCase(1620777600)]
[TestCase(1620777600.000)]
public void TestDateTimeConverterFromSeconds(double input)
{
var output = DateTimeConverter.ConvertFromSeconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToSeconds()
{
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600);
}
[TestCase(1620777600000)]
[TestCase(1620777600000.000)]
public void TestDateTimeConverterFromMilliseconds(double input)
{
var output = DateTimeConverter.ConvertFromMilliseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToMilliseconds()
{
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000);
}
[TestCase(1620777600000000)]
public void TestDateTimeConverterFromMicroseconds(long input)
{
var output = DateTimeConverter.ConvertFromMicroseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToMicroseconds()
{
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000000);
}
[TestCase(1620777600000000000)]
public void TestDateTimeConverterFromNanoseconds(long input)
{
var output = DateTimeConverter.ConvertFromNanoseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToNanoseconds()
{
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000000000);
}
[TestCase()]
public void TestDateTimeConverterNull()
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": null }}");
Assert.That(output!.Time == null);
}
}
public class STJTimeObject
{
[JsonConverter(typeof(DateTimeConverter))]
[JsonPropertyName("time")]
public DateTime? Time { get; set; }
}
}
@@ -1,49 +0,0 @@
using CryptoExchange.Net.Converters.SystemTextJson;
using NUnit.Framework;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
public class DecimalConverterTests
{
[TestCase("1", 1)]
[TestCase("1.1", 1.1)]
[TestCase("-1.1", -1.1)]
[TestCase(null, null)]
[TestCase("", null)]
[TestCase("null", null)]
[TestCase("nan", null)]
[TestCase("1E+2", 100)]
[TestCase("1E-2", 0.01)]
[TestCase("Infinity", 999)] // 999 is workaround for not being able to specify decimal.MinValue
[TestCase("-Infinity", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
[TestCase("80228162514264337593543950335", 999)] // 999 is workaround for not being able to specify decimal.MaxValue
[TestCase("-80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
public void TestDecimalConverterString(string value, decimal? expected)
{
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \"" + value + "\"}");
Assert.That(result!.Test, Is.EqualTo(expected == -999 ? decimal.MinValue : expected == 999 ? decimal.MaxValue : expected));
}
[TestCase("1", 1)]
[TestCase("1.1", 1.1)]
[TestCase("-1.1", -1.1)]
[TestCase("null", null)]
[TestCase("1E+2", 100)]
[TestCase("1E-2", 0.01)]
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
public void TestDecimalConverterNumber(string value, decimal? expected)
{
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": " + value + "}");
Assert.That(result!.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
}
}
public class STJDecimalObject
{
[JsonConverter(typeof(DecimalConverter))]
[JsonPropertyName("test")]
public decimal? Test { get; set; }
}
}
@@ -1,147 +0,0 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Testing;
using NUnit.Framework;
using System;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
public class EnumConverterTests
{
[TestCase(TestEnum.One, "1")]
[TestCase(TestEnum.Two, "2")]
[TestCase(TestEnum.Three, "three")]
[TestCase(TestEnum.Four, "Four")]
[TestCase(null, null)]
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
{
var output = EnumConverter.GetString(value);
Assert.That(output == expected);
}
[TestCase(TestEnum.One, "1")]
[TestCase(TestEnum.Two, "2")]
[TestCase(TestEnum.Three, "three")]
[TestCase(TestEnum.Four, "Four")]
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
{
var output = EnumConverter.GetString(value);
Assert.That(output == expected);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", null)]
[TestCase(null, null)]
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new TestSerializerContext()));
Assert.That(output!.Value == expected);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", (TestEnum)(-9))]
[TestCase(null, (TestEnum)(-9))]
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<NotNullableSTJEnumObject>($"{{ \"Value\": {val} }}");
Assert.That(output!.Value == expected);
}
[Test]
public void TestEnumConverterMapsUndefinedValueCorrectlyIfDefaultIsDefined()
{
var output = JsonSerializer.Deserialize<TestEnum2>($"\"TestUndefined\"");
Assert.That((int)output == -99);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", null)]
[TestCase(null, null)]
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
{
var result = EnumConverter.ParseString<TestEnum>(value);
Assert.That(result == expected);
}
[Test]
public void TestEnumConverterParseNullOnNonNullableOnlyLogsOnce()
{
LibraryHelpers.StaticLogger = new TraceLogger();
var listener = new EnumValueTraceListener();
Trace.Listeners.Add(listener);
EnumConverter<TestEnum>.Reset();
try
{
Assert.Throws<Exception>(() =>
{
var result = JsonSerializer.Deserialize<NotNullableSTJEnumObject>("{\"Value\": null}", SerializerOptions.WithConverters(new TestSerializerContext()));
});
Assert.DoesNotThrow(() =>
{
var result2 = JsonSerializer.Deserialize<NotNullableSTJEnumObject>("{\"Value\": null}", SerializerOptions.WithConverters(new TestSerializerContext()));
});
}
finally
{
Trace.Listeners.Remove(listener);
}
}
}
public class STJEnumObject
{
public TestEnum? Value { get; set; }
}
public class NotNullableSTJEnumObject
{
public TestEnum Value { get; set; }
}
[JsonConverter(typeof(EnumConverter<TestEnum>))]
public enum TestEnum
{
[Map("1")]
One,
[Map("2")]
Two,
[Map("three", "3")]
Three,
Four
}
[JsonConverter(typeof(EnumConverter<TestEnum2>))]
public enum TestEnum2
{
[Map("-9")]
Minus9 = -9,
[Map("1")]
One,
[Map("2")]
Two,
[Map("three", "3")]
Three,
Four
}
}
@@ -1,46 +0,0 @@
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
using System.Text.Json;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
[TestFixture()]
public class SharedModelConversionTests
{
[TestCase(TradingMode.Spot, "ETH", "USDT", null)]
[TestCase(TradingMode.PerpetualLinear, "ETH", "USDT", null)]
[TestCase(TradingMode.DeliveryLinear, "ETH", "USDT", 1748432430)]
public void TestSharedSymbolConversion(TradingMode tradingMode, string baseAsset, string quoteAsset, int? deliverTime)
{
DateTime? time = deliverTime == null ? null : DateTimeConverter.ParseFromDouble(deliverTime.Value);
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, time);
var serialized = JsonSerializer.Serialize(symbol);
var restored = JsonSerializer.Deserialize<SharedSymbol>(serialized);
Assert.That(restored!.TradingMode, Is.EqualTo(symbol.TradingMode));
Assert.That(restored.BaseAsset, Is.EqualTo(symbol.BaseAsset));
Assert.That(restored.QuoteAsset, Is.EqualTo(symbol.QuoteAsset));
Assert.That(restored.DeliverTime, Is.EqualTo(symbol.DeliverTime));
}
[TestCase(0.1, null, null)]
[TestCase(0.1, 0.1, null)]
[TestCase(0.1, 0.1, 0.1)]
[TestCase(null, 0.1, null)]
[TestCase(null, 0.1, 0.1)]
public void TestSharedQuantityConversion(double? baseQuantity, double? quoteQuantity, double? contractQuantity)
{
var symbol = new SharedOrderQuantity((decimal?)baseQuantity, (decimal?)quoteQuantity, (decimal?)contractQuantity);
var serialized = JsonSerializer.Serialize(symbol);
var restored = JsonSerializer.Deserialize<SharedOrderQuantity>(serialized);
Assert.That(restored!.QuantityInBaseAsset, Is.EqualTo(symbol.QuantityInBaseAsset));
Assert.That(restored.QuantityInQuoteAsset, Is.EqualTo(symbol.QuantityInQuoteAsset));
Assert.That(restored.QuantityInContracts, Is.EqualTo(symbol.QuantityInContracts));
}
}
}
@@ -1,16 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"></PackageReference>
<None Include="..\CryptoExchange.Net\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"></PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.4.0"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="6.0.1"></PackageReference>
<PackageReference Include="NUnit" Version="4.3.2"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"></PackageReference>
</ItemGroup>
<ItemGroup>
@@ -1,5 +1,7 @@
using CryptoExchange.Net.Objects;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System.Diagnostics;
using System.Globalization;
namespace CryptoExchange.Net.UnitTests
@@ -30,7 +32,6 @@ namespace CryptoExchange.Net.UnitTests
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
[TestCase(0, 1, 0.000000001, RoundingType.Closest, 0.0000097232, 0.000009723)]
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
{
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
@@ -1,465 +0,0 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
using System.Linq;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class ExchangeSymbolCacheTests
{
private SharedSpotSymbol[] CreateTestSymbols()
{
return new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("BTC", "EUR", "BTCEUR", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "BTC", "ETHBTC", true, TradingMode.Spot),
new SharedSpotSymbol("XRP", "USDT", "XRPUSDT", false, TradingMode.Spot)
};
}
private SharedSpotSymbol[] CreateFuturesSymbols()
{
return new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT-PERP", true, TradingMode.PerpetualLinear),
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT-PERP", true, TradingMode.PerpetualLinear)
};
}
[Test]
public void UpdateSymbolInfo_NewTopic_Should_AddToCache()
{
// arrange
var topicId = "NewExchange";
var symbols = CreateTestSymbols();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId);
// assert
Assert.That(hasCached, Is.True);
}
[Test]
public void UpdateSymbolInfo_Should_StoreAllSymbols()
{
// arrange
var topicId = "ExchangeWithSymbols";
var symbols = CreateTestSymbols();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, 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);
}
[Test]
public void UpdateSymbolInfo_CalledTwiceWithinAnHour_Should_NotUpdate()
{
// arrange
var topicId = "ExchangeNoUpdate";
var initialSymbols = new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot)
};
var updatedSymbols = new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot)
};
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, initialSymbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, updatedSymbols);
// assert - should still have only the initial symbol since less than 60 minutes passed
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True);
// The second update should not have been applied
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.False);
}
[Test]
public void UpdateSymbolInfo_WithEmptyArray_Should_CreateEmptyCache()
{
// arrange
var topicId = "EmptyExchange";
var symbols = Array.Empty<SharedSpotSymbol>();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId);
// assert
Assert.That(hasCached, Is.False);
}
[Test]
public void HasCached_NonExistentTopic_Should_ReturnFalse()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.HasCached(nonExistentTopic);
// assert
Assert.That(result, Is.False);
}
[Test]
public void HasCached_ExistingTopicWithSymbols_Should_ReturnTrue()
{
// arrange
var topicId = "ExchangeWithData";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.HasCached(topicId);
// assert
Assert.That(result, Is.True);
}
[Test]
public void HasCached_ExistingTopicWithNoSymbols_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeNoData";
ExchangeSymbolCache.UpdateSymbolInfo(topicId, Array.Empty<SharedSpotSymbol>());
// act
var result = ExchangeSymbolCache.HasCached(topicId);
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_ByName_ExistingSymbol_Should_ReturnTrue()
{
// arrange
var topicId = "ExchangeSupports";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT");
// assert
Assert.That(result, Is.True);
}
[Test]
public void SupportsSymbol_ByName_NonExistingSymbol_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeNoSupport";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "LINKUSDT");
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_ByName_NonExistentTopic_Should_ReturnFalse()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "BTCUSDT");
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_BySharedSymbol_ExistingSymbol_Should_ReturnTrue()
{
// arrange
var topicId = "ExchangeSharedSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
// assert
Assert.That(result, Is.True);
}
[Test]
public void SupportsSymbol_BySharedSymbol_NonExistingSymbol_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeNoSharedSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "LINK", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_BySharedSymbol_DifferentTradingMode_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeDifferentMode";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_BySharedSymbol_NonExistentTopic_Should_ReturnFalse()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, sharedSymbol);
// assert
Assert.That(result, Is.False);
}
[Test]
public void GetSymbolsForBaseAsset_ExistingBaseAsset_Should_ReturnMatchingSymbols()
{
// arrange
var topicId = "ExchangeBaseAsset";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(2));
Assert.That(result.Any(x => x.QuoteAsset == "USDT"), Is.True);
Assert.That(result.Any(x => x.QuoteAsset == "EUR"), Is.True);
}
[Test]
public void GetSymbolsForBaseAsset_CaseInsensitive_Should_ReturnMatchingSymbols()
{
// arrange
var topicId = "ExchangeCaseInsensitive";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "btc");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(2));
}
[Test]
public void GetSymbolsForBaseAsset_NonExistingBaseAsset_Should_ReturnEmptyArray()
{
// arrange
var topicId = "ExchangeNoBaseAsset";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "LINK");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(0));
}
[Test]
public void GetSymbolsForBaseAsset_NonExistentTopic_Should_ReturnEmptyArray()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(nonExistentTopic, "BTC");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(0));
}
[Test]
public void ParseSymbol_ExistingSymbol_Should_ReturnSharedSymbol()
{
// arrange
var topicId = "ExchangeParse";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "BTCUSDT");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result!.BaseAsset, Is.EqualTo("BTC"));
Assert.That(result.QuoteAsset, Is.EqualTo("USDT"));
Assert.That(result.TradingMode, Is.EqualTo(TradingMode.Spot));
Assert.That(result.SymbolName, Is.EqualTo("BTCUSDT"));
}
[Test]
public void ParseSymbol_NonExistingSymbol_Should_ReturnNull()
{
// arrange
var topicId = "ExchangeNoParse";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "LINKUSDT");
// assert
Assert.That(result, Is.Null);
}
[Test]
public void ParseSymbol_NullSymbolName_Should_ReturnNull()
{
// arrange
var topicId = "ExchangeNullSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, null);
// assert
Assert.That(result, Is.Null);
}
[Test]
public void ParseSymbol_NonExistentTopic_Should_ReturnNull()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.ParseSymbol(nonExistentTopic, "BTCUSDT");
// assert
Assert.That(result, Is.Null);
}
[Test]
public void MultipleTopics_Should_MaintainSeparateData()
{
// arrange
var topic1 = "Exchange1";
var topic2 = "Exchange2";
var symbols1 = new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot)
};
var symbols2 = new[]
{
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot)
};
// act
ExchangeSymbolCache.UpdateSymbolInfo(topic1, symbols1);
ExchangeSymbolCache.UpdateSymbolInfo(topic2, 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);
}
[Test]
public void UpdateSymbolInfo_WithDifferentTradingModes_Should_StoreCorrectly()
{
// arrange
var topicId = "ExchangeMixedModes";
var spotSymbols = CreateTestSymbols();
var futuresSymbols = CreateFuturesSymbols();
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, 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);
}
[Test]
public void GetSymbolsForBaseAsset_Should_ReturnAllTradingModes()
{
// arrange
var topicId = "ExchangeAllModes";
var spotSymbols = CreateTestSymbols();
var futuresSymbols = CreateFuturesSymbols();
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
// assert
Assert.That(result.Length, Is.GreaterThanOrEqualTo(2));
Assert.That(result.Any(x => x.TradingMode == TradingMode.Spot), Is.True);
Assert.That(result.Any(x => x.TradingMode == TradingMode.PerpetualLinear), Is.True);
}
[Test]
public void GetSymbolsForBaseAsset_WithMultipleMatchingSymbols_Should_ReturnAll()
{
// arrange
var topicId = "ExchangeMultiple";
var symbols = new[]
{
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "BTC", "ETHBTC", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "EUR", "ETHEUR", true, TradingMode.Spot)
};
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "ETH");
// assert
Assert.That(result.Length, Is.EqualTo(3));
Assert.That(result.All(x => x.BaseAsset == "ETH"), Is.True);
}
}
}
@@ -1,20 +0,0 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Objects;
using System.Collections.Generic;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestAuthenticationProvider : AuthenticationProvider<TestCredentials, TestCredentials>
{
public TestAuthenticationProvider(TestCredentials credentials) : base(credentials, credentials)
{
}
public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig)
{
requestConfig.Headers ??= new Dictionary<string, string>();
requestConfig.Headers["Authorization"] = Credential.Key;
}
}
}
@@ -1,30 +0,0 @@
using CryptoExchange.Net.Authentication;
using System;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestCredentials : HMACCredential
{
public TestCredentials() { }
public TestCredentials(string key, string secret) : base(key, secret)
{
}
public TestCredentials(HMACCredential credential) : base(credential.Key, credential.Secret)
{
}
public TestCredentials WithHMAC(string key, string secret)
{
if (!string.IsNullOrEmpty(Key)) throw new InvalidOperationException("Credentials already set");
Key = key;
Secret = secret;
return this;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new TestCredentials(this);
}
}
@@ -1,64 +0,0 @@
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestEnvironment : TradeEnvironment
{
public string RestClientAddress { get; }
public string SocketClientAddress { get; }
internal TestEnvironment(
string name,
string restAddress,
string streamAddress) :
base(name)
{
RestClientAddress = restAddress;
SocketClientAddress = streamAddress;
}
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
public TestEnvironment() : base(TradeEnvironmentNames.Live)
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
{ }
/// <summary>
/// Get the environment by name
/// </summary>
public static TestEnvironment? GetEnvironmentByName(string? name)
=> name switch
{
TradeEnvironmentNames.Live => Live,
"" => Live,
null => Live,
_ => default
};
/// <summary>
/// Available environment names
/// </summary>
/// <returns></returns>
public static string[] All => [Live.Name];
/// <summary>
/// Live environment
/// </summary>
public static TestEnvironment Live { get; }
= new TestEnvironment(TradeEnvironmentNames.Live,
"https://localhost",
"wss://localhost");
/// <summary>
/// Create a custom environment
/// </summary>
/// <param name="name"></param>
/// <param name="spotRestAddress"></param>
/// <param name="spotSocketStreamsAddress"></param>
/// <returns></returns>
public static TestEnvironment CreateCustom(
string name,
string spotRestAddress,
string spotSocketStreamsAddress)
=> new TestEnvironment(name, spotRestAddress, spotSocketStreamsAddress);
}
}
@@ -1,25 +0,0 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using System;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestQuery : Query<TestSocketMessage>
{
public TestQuery(TestSocketMessage request, bool authenticated) : base(request, authenticated, 1)
{
MessageRouter = MessageRouter.CreateWithoutTopicFilter<TestSocketMessage>(request.Id.ToString(), HandleMessage);
}
private CallResult? 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.SuccessResult;
}
}
}
@@ -1,66 +0,0 @@
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Testing.Implementations;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestRestApiClient : RestApiClient<TestEnvironment, TestAuthenticationProvider, TestCredentials>
{
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 override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null) =>
baseAsset + quoteAsset;
protected override TestAuthenticationProvider CreateAuthenticationProvider(TestCredentials credentials) =>
new TestAuthenticationProvider(credentials);
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(SerializerOptions.WithConverters(new TestSerializerContext()));
internal void SetNextResponse(string data, HttpStatusCode code)
{
var expectedBytes = Encoding.UTF8.GetBytes(data);
var responseStream = new MemoryStream();
responseStream.Write(expectedBytes, 0, expectedBytes.Length);
responseStream.Seek(0, SeekOrigin.Begin);
var response = new TestResponse(code, responseStream);
var request = new TestRequest(response);
var factory = new TestRequestFactory(request);
RequestFactory = factory;
}
internal async Task<WebCallResult<T>> GetResponseAsync<T>(HttpMethod? httpMethod = null, ParameterCollection? collection = null, RateLimitGate? rateLimitGate = null)
{
var definition = new RequestDefinition("/path", httpMethod ?? HttpMethod.Get)
{
Weight = rateLimitGate == null ? 0 : 1,
RateLimitGate = rateLimitGate
};
return await SendAsync<T>(BaseAddress, definition, collection ?? new ParameterCollection(), default);
}
internal void SetParameterPosition(HttpMethod httpMethod, HttpMethodParameterPosition pos)
{
ParameterPositions[httpMethod] = pos;
}
}
}
@@ -1,27 +0,0 @@
using CryptoExchange.Net.Clients;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Net.Http;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestRestClient : BaseRestClient<TestEnvironment, TestCredentials>
{
public TestRestApiClient ApiClient1 { get; set; }
public TestRestApiClient ApiClient2 { get; set; }
public TestRestClient(Action<TestRestOptions>? optionsDelegate = null)
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
{
}
public TestRestClient(HttpClient? httpClient, ILoggerFactory? loggerFactory, IOptions<TestRestOptions> options) : base(loggerFactory, "Test")
{
Initialize(options.Value);
ApiClient1 = AddApiClient(new TestRestApiClient(_logger, httpClient, options.Value));
ApiClient2 = AddApiClient(new TestRestApiClient(_logger, httpClient, options.Value));
}
}
}
@@ -1,33 +0,0 @@
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using System.IO;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestRestMessageHandler : JsonRestMessageHandler
{
public override JsonSerializerOptions Options { get; } = SerializerOptions.WithConverters(new TestSerializerContext());
public override async ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
{
var (jsonError, jsonDocument) = await GetJsonDocument(responseStream).ConfigureAwait(false);
if (jsonError != null)
return jsonError;
int? code = jsonDocument!.RootElement.TryGetProperty("errorCode", out var codeProp) ? codeProp.GetInt32() : null;
var msg = jsonDocument.RootElement.TryGetProperty("errorMessage", out var msgProp) ? msgProp.GetString() : null;
if (msg == null)
return new ServerError(ErrorInfo.Unknown);
if (code == null)
return new ServerError(ErrorInfo.Unknown with { Message = msg });
return new ServerError(code.Value, new ErrorInfo(ErrorType.Unknown, false, "Error") with { Message = msg });
}
}
}
@@ -1,27 +0,0 @@
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestRestOptions : RestExchangeOptions<TestEnvironment, TestCredentials>
{
internal static TestRestOptions Default { get; set; } = new TestRestOptions()
{
Environment = TestEnvironment.Live,
AutoTimestamp = true
};
public TestRestOptions()
{
Default?.Set(this);
}
public RestApiOptions ExchangeOptions { get; private set; } = new RestApiOptions();
internal TestRestOptions Set(TestRestOptions targetOptions)
{
targetOptions = base.Set<TestRestOptions>(targetOptions);
targetOptions.ExchangeOptions = ExchangeOptions.Set(targetOptions.ExchangeOptions);
return targetOptions;
}
}
}
@@ -1,29 +0,0 @@
using CryptoExchange.Net.UnitTests.ConverterTests;
using CryptoExchange.Net.UnitTests.Implementations;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests
{
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(IDictionary<string, string>))]
[JsonSerializable(typeof(Dictionary<string, object>))]
[JsonSerializable(typeof(IDictionary<string, object>))]
[JsonSerializable(typeof(TestObject))]
[JsonSerializable(typeof(TestSocketMessage))]
[JsonSerializable(typeof(Test))]
[JsonSerializable(typeof(Test2))]
[JsonSerializable(typeof(Test3))]
[JsonSerializable(typeof(NotNullableSTJBoolObject))]
[JsonSerializable(typeof(STJBoolObject))]
[JsonSerializable(typeof(NotNullableSTJEnumObject))]
[JsonSerializable(typeof(STJEnumObject))]
[JsonSerializable(typeof(STJDecimalObject))]
[JsonSerializable(typeof(STJTimeObject))]
internal partial class TestSerializerContext : JsonSerializerContext
{
}
}
@@ -1,44 +0,0 @@
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging;
using System;
using System.Net.Http;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
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(ILogger logger, HttpClient httpClient, string baseAddress, TestSocketOptions options, SocketApiOptions apiOptions)
: base(logger, baseAddress, options, apiOptions)
{
}
public override ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType) => new TestSocketMessageHandler();
protected internal override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(SerializerOptions.WithConverters(new TestSerializerContext()));
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null) =>
baseAsset + quoteAsset;
protected override TestAuthenticationProvider CreateAuthenticationProvider(TestCredentials credentials) =>
new TestAuthenticationProvider(credentials);
public async Task<CallResult<UpdateSubscription>> SubscribeToUpdatesAsync<T>(Action<DataEvent<T>> handler, bool subQuery, CancellationToken ct)
{
return await base.SubscribeAsync(new TestSubscription<T>(_logger, handler, subQuery, false), ct);
}
}
}
@@ -1,26 +0,0 @@
using CryptoExchange.Net.Clients;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestSocketClient : BaseSocketClient<TestEnvironment, TestCredentials>
{
public TestSocketApiClient ApiClient1 { get; set; }
public TestSocketApiClient ApiClient2 { get; set; }
public TestSocketClient(Action<TestSocketOptions>? optionsDelegate = null)
: this(null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
{
}
public TestSocketClient(ILoggerFactory? loggerFactory, IOptions<TestSocketOptions> options) : base(loggerFactory, "Test")
{
Initialize(options.Value);
ApiClient1 = AddApiClient(new TestSocketApiClient(_logger, options.Value));
ApiClient2 = AddApiClient(new TestSocketApiClient(_logger, options.Value));
}
}
}
@@ -1,16 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal record TestSocketMessage
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("data")]
public string Data { get; set; } = string.Empty;
}
}
@@ -1,33 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
using System.Text.Json;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestSocketMessageHandler : JsonSocketMessageHandler
{
public override JsonSerializerOptions Options { get; } = SerializerOptions.WithConverters(new TestSerializerContext());
public TestSocketMessageHandler()
{
}
protected override MessageTypeDefinition[] TypeEvaluators { get; } = [
new MessageTypeDefinition {
ForceIfFound = true,
Fields = [
new PropertyFieldReference("id")
],
TypeIdentifierCallback = (doc) => doc.FieldValue("id")!
},
new MessageTypeDefinition {
Fields = [
],
StaticIdentifier = "test"
},
];
}
}
@@ -1,27 +0,0 @@
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestSocketOptions : SocketExchangeOptions<TestEnvironment, TestCredentials>
{
internal static TestSocketOptions Default { get; set; } = new TestSocketOptions()
{
Environment = TestEnvironment.Live,
AutoTimestamp = true
};
public TestSocketOptions()
{
Default?.Set(this);
}
public SocketApiOptions ExchangeOptions { get; private set; } = new SocketApiOptions();
internal TestSocketOptions Set(TestSocketOptions targetOptions)
{
targetOptions = base.Set<TestSocketOptions>(targetOptions);
targetOptions.ExchangeOptions = ExchangeOptions.Set(targetOptions.ExchangeOptions);
return targetOptions;
}
}
}
@@ -1,50 +0,0 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Reflection.Metadata;
using System.Text;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestSubscription<T> : Subscription
{
private readonly Action<DataEvent<T>> _handler;
private bool _subQuery;
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler, bool subQuery, bool authenticated) : base(logger, authenticated, true)
{
_handler = handler;
_subQuery = subQuery;
MessageRouter = MessageRouter.CreateWithoutTopicFilter<T>("test", HandleUpdate);
}
protected override Query? GetSubQuery(SocketConnection connection)
{
if (!_subQuery)
return null;
return new TestQuery(new TestSocketMessage { Id = 1, Data = "Sub" }, false);
}
protected override Query? GetUnsubQuery(SocketConnection connection)
{
if (!_subQuery)
return null;
return new TestQuery(new TestSocketMessage { Id = 2, Data = "Unsub" }, false);
}
private CallResult? HandleUpdate(SocketConnection connection, DateTime time, string? originalData, T data)
{
_handler(new DataEvent<T>("Test", data, time, originalData));
return CallResult.SuccessResult;
}
}
}
+102 -84
View File
@@ -1,9 +1,15 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.UnitTests.Implementations;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
@@ -11,9 +17,9 @@ namespace CryptoExchange.Net.UnitTests
public class OptionsTests
{
[TearDown]
public void TearDown()
public void Init()
{
TestRestOptions.Default = new TestRestOptions
TestClientOptions.Default = new TestClientOptions
{
};
}
@@ -30,123 +36,135 @@ namespace CryptoExchange.Net.UnitTests
// act
// assert
Assert.Throws(typeof(ArgumentException),
() => {
var opts = new TestRestOptions()
{
ApiCredentials = new TestCredentials(key, secret)
};
opts.ApiCredentials.Validate();
});
() => new RestExchangeOptions<TestEnvironment, ApiCredentials>() { ApiCredentials = new ApiCredentials(key, secret) });
}
[Test]
public void TestBasicOptionsAreSet()
{
// arrange, act
var options = new TestRestOptions
var options = new TestClientOptions
{
ApiCredentials = new TestCredentials("123", "456"),
RequestTimeout = TimeSpan.FromSeconds(10)
ApiCredentials = new ApiCredentials("123", "456"),
ReceiveWindow = TimeSpan.FromSeconds(10)
};
// assert
Assert.That(options.RequestTimeout == TimeSpan.FromSeconds(10));
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10));
Assert.That(options.ApiCredentials.Key == "123");
Assert.That(options.ApiCredentials.Secret == "456");
}
[Test]
public void TestSetOptionsRest()
public void TestApiOptionsAreSet()
{
var client = new TestRestClient();
client.SetOptions(new UpdateOptions
{
RequestTimeout = TimeSpan.FromSeconds(2),
Proxy = new ApiProxy("http://testproxy", 1234)
});
// arrange, act
var options = new TestClientOptions();
options.Api1Options.ApiCredentials = new ApiCredentials("123", "456");
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
Assert.That(client.ApiClient1.ClientOptions.Proxy, Is.Not.Null);
Assert.That(client.ApiClient1.ClientOptions.Proxy!.Host, Is.EqualTo("http://testproxy"));
Assert.That(client.ApiClient1.ClientOptions.Proxy.Port, Is.EqualTo(1234));
Assert.That(client.ApiClient1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
// assert
Assert.That(options.Api1Options.ApiCredentials.Key == "123");
Assert.That(options.Api1Options.ApiCredentials.Secret == "456");
Assert.That(options.Api2Options.ApiCredentials.Key == "789");
Assert.That(options.Api2Options.ApiCredentials.Secret == "101");
}
[Test]
public void TestSetOptionsRestWithCredentials()
public void TestClientUsesCorrectOptions()
{
var client = new TestRestClient();
client.SetOptions(new UpdateOptions<TestCredentials>
{
ApiCredentials = new TestCredentials("123", "456"),
RequestTimeout = TimeSpan.FromSeconds(2),
Proxy = new ApiProxy("http://testproxy", 1234)
var client = new TestRestClient(options => {
options.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
options.ApiCredentials = new ApiCredentials("333", "444");
});
Assert.That(client.ApiClient1.ApiCredentials, Is.Not.Null);
Assert.That(client.ApiClient1.ApiCredentials!.Key, Is.EqualTo("123"));
Assert.That(client.ApiClient1.ClientOptions.Proxy, Is.Not.Null);
Assert.That(client.ApiClient1.ClientOptions.Proxy!.Host, Is.EqualTo("http://testproxy"));
Assert.That(client.ApiClient1.ClientOptions.Proxy.Port, Is.EqualTo(1234));
Assert.That(client.ApiClient1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
Assert.That(authProvider1.GetKey() == "111");
Assert.That(authProvider1.GetSecret() == "222");
Assert.That(authProvider2.GetKey() == "333");
Assert.That(authProvider2.GetSecret() == "444");
}
[Test]
public void TestWhenUpdatingSettingsExistingClientsAreNotAffected()
public void TestClientUsesCorrectOptionsWithDefault()
{
TestRestOptions.Default = new TestRestOptions
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
var client = new TestRestClient();
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
Assert.That(authProvider1.GetKey() == "111");
Assert.That(authProvider1.GetSecret() == "222");
Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456");
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
}
[Test]
public void TestClientUsesCorrectOptionsWithOverridingDefault()
{
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
var client = new TestRestClient(options =>
{
ApiCredentials = new TestCredentials("111", "222"),
RequestTimeout = TimeSpan.FromSeconds(1),
};
options.Api1Options.ApiCredentials = new ApiCredentials("333", "444");
options.Environment = new TestEnvironment("Test", "https://test.test");
});
var client1 = new TestRestClient();
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
Assert.That(authProvider1.GetKey() == "333");
Assert.That(authProvider1.GetSecret() == "444");
Assert.That(authProvider2.GetKey() == "123");
Assert.That(authProvider2.GetSecret() == "456");
Assert.That(client.Api2.BaseAddress == "https://localhost:123");
Assert.That(client1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(1)));
Assert.That(client1.ClientOptions.ApiCredentials!.Key, Is.EqualTo("111"));
TestRestOptions.Default.ApiCredentials = new TestCredentials("333", "444");
TestRestOptions.Default.RequestTimeout = TimeSpan.FromSeconds(2);
var client2 = new TestRestClient();
Assert.That(client2.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
Assert.That(client2.ClientOptions.ApiCredentials!.Key, Is.EqualTo("333"));
// Cleanup static values
TestClientOptions.Default.ApiCredentials = null;
TestClientOptions.Default.Api1Options.ApiCredentials = null;
}
}
//public class TestClientOptions: RestExchangeOptions<TestEnvironment, HMACCredential>
//{
// /// <summary>
// /// Default options for the futures client
// /// </summary>
// public static TestClientOptions Default { get; set; } = new TestClientOptions()
// {
// Environment = new TestEnvironment("test", "https://test.com")
// };
public class TestClientOptions: RestExchangeOptions<TestEnvironment, ApiCredentials>
{
/// <summary>
/// Default options for the futures client
/// </summary>
public static TestClientOptions Default { get; set; } = new TestClientOptions()
{
Environment = new TestEnvironment("test", "https://test.com")
};
// /// <summary>
// /// ctor
// /// </summary>
// public TestClientOptions()
// {
// Default?.Set(this);
// }
/// <summary>
/// ctor
/// </summary>
public TestClientOptions()
{
Default?.Set(this);
}
// /// <summary>
// /// The default receive window for requests
// /// </summary>
// public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// The default receive window for requests
/// </summary>
public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
// public RestApiOptions Api1Options { get; private set; } = new RestApiOptions();
public RestApiOptions Api1Options { get; private set; } = new RestApiOptions();
// public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
// internal TestClientOptions Set(TestClientOptions targetOptions)
// {
// targetOptions = base.Set<TestClientOptions>(targetOptions);
// targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
// targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
// return targetOptions;
// }
//}
internal TestClientOptions Set(TestClientOptions targetOptions)
{
targetOptions = base.Set<TestClientOptions>(targetOptions);
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
return targetOptions;
}
}
}
@@ -1,331 +0,0 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.ConverterTests;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.UnitTests
{
internal class ParameterCollectionTests
{
[Test]
public void AddingBasicValue_SetValueCorrectly()
{
var parameters = new ParameterCollection();
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);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingDecimalValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddString("test", 0.1m);
Assert.That(parameters["test"], Is.EqualTo("0.1"));
}
[Test]
public void AddingOptionalDecimalValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("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);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingLongValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddString("test", 1L);
Assert.That(parameters["test"], Is.EqualTo("1"));
}
[Test]
public void AddingOptionalLongValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", 1L);
Assert.That(parameters["test"], Is.EqualTo("1"));
}
[Test]
public void AddingOptionalLongNullValueAsString_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("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));
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));
Assert.That(parameters["test"], Is.EqualTo(1735689600000));
}
[Test]
public void AddingOptionalMillisecondNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalMilliseconds("test", 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));
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));
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));
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));
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));
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));
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);
Assert.That(parameters["test"], Is.EqualTo("2"));
}
[Test]
public void AddingOptionalEnum_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalEnum("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);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingEnumAsInt_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddEnumAsInt("test", TestEnum.Two);
Assert.That(parameters["test"], Is.EqualTo(2));
}
[Test]
public void AddingOptionalEnumAsInt_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalEnumAsInt("test", (TestEnum?)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();
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);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingCommaSeparatedEnum_SetValueCorrectly()
{
var parameters = new ParameterCollection();
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);
Assert.That(parameters["test"], Is.EqualTo("true"));
}
[Test]
public void AddingOptionalBoolString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalBoolString("test", true);
Assert.That(parameters["test"], Is.EqualTo("true"));
}
[Test]
public void AddingOptionalBoolStringNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalBoolString("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
}
}
@@ -1,23 +1,160 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Filters;
using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.UnitTests.Implementations;
using CryptoExchange.Net.UnitTests.TestImplementations;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Threading;
using NUnit.Framework.Legacy;
using CryptoExchange.Net.RateLimiting;
using System.Net;
using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Filters;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Text.Json;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class RateLimitTests
public class RestClientTests
{
[TestCase]
public void RequestingData_Should_ResultInData()
{
// arrange
var client = new TestRestClient();
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
client.SetResponse(JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() }), out _);
// act
var result = client.Api1.Request<TestObject>().Result;
// assert
Assert.That(result.Success);
Assert.That(TestHelpers.AreEqual(expected, result.Data));
}
[TestCase]
public void ReceivingInvalidData_Should_ResultInError()
{
// arrange
var client = new TestRestClient();
client.SetResponse("{\"property\": 123", out _);
// act
var result = client.Api1.Request<TestObject>().Result;
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase]
public async Task ReceivingErrorCode_Should_ResultInError()
{
// arrange
var client = new TestRestClient();
client.SetErrorWithoutResponse(System.Net.HttpStatusCode.BadRequest, "Invalid request");
// act
var result = await client.Api1.Request<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase]
public async Task ReceivingErrorAndNotParsingError_Should_ResultInFlatError()
{
// arrange
var client = new TestRestClient();
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.Api1.Request<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
}
[TestCase]
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
{
// arrange
var client = new ParseErrorTestRestClient();
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.Api2.Request<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
Assert.That(result.Error.ErrorCode == "123");
Assert.That(result.Error.Message == "Invalid request");
}
[TestCase]
public void SettingOptions_Should_ResultInOptionsSet()
{
// arrange
// act
var options = new TestClientOptions();
options.Api1Options.TimestampRecalculationInterval = TimeSpan.FromMinutes(10);
options.Api1Options.OutputOriginalData = true;
options.RequestTimeout = TimeSpan.FromMinutes(1);
var client = new TestBaseClient(options);
// assert
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.TimestampRecalculationInterval == TimeSpan.FromMinutes(10));
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.OutputOriginalData == true);
Assert.That(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
}
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
[TestCase("POST", HttpMethodParameterPosition.InBody)]
[TestCase("POST", HttpMethodParameterPosition.InUri)]
[TestCase("DELETE", HttpMethodParameterPosition.InBody)]
[TestCase("DELETE", HttpMethodParameterPosition.InUri)]
[TestCase("PUT", HttpMethodParameterPosition.InUri)]
[TestCase("PUT", HttpMethodParameterPosition.InBody)]
public async Task Setting_Should_ResultInOptionsSet(string method, HttpMethodParameterPosition pos)
{
// arrange
// act
var client = new TestRestClient();
client.Api1.SetParameterPosition(new HttpMethod(method), pos);
client.SetResponse("{}", out var request);
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new ParameterCollection
{
{ "TestParam1", "Value1" },
{ "TestParam2", 2 },
},
new Dictionary<string, string>
{
{ "TestHeader", "123" }
});
// assert
Assert.That(request.Method == new HttpMethod(method));
Assert.That((request.Content?.Contains("TestParam1") == true) == (pos == HttpMethodParameterPosition.InBody));
Assert.That((request.Uri.ToString().Contains("TestParam1")) == (pos == HttpMethodParameterPosition.InUri));
Assert.That((request.Content?.Contains("TestParam2") == true) == (pos == HttpMethodParameterPosition.InBody));
Assert.That((request.Uri.ToString().Contains("TestParam2")) == (pos == HttpMethodParameterPosition.InUri));
Assert.That(request.GetHeaders().First().Key == "TestHeader");
Assert.That(request.GetHeaders().First().Value.Contains("123"));
}
[TestCase(1, 0.1)]
[TestCase(2, 0.1)]
[TestCase(5, 1)]
@@ -33,12 +170,12 @@ namespace CryptoExchange.Net.UnitTests
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);
Assert.That(i == requests ? triggered : !triggered);
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "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, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(!triggered);
}
@@ -54,12 +191,12 @@ namespace CryptoExchange.Net.UnitTests
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
RateLimitEvent? evnt = null;
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);
bool expected = i == 1 ? expectLimiting ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected);
}
}
@@ -76,7 +213,7 @@ namespace CryptoExchange.Net.UnitTests
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get);
RateLimitEvent? evnt = null;
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);
@@ -116,15 +253,15 @@ 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);
RateLimitEvent? evnt = null;
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);
bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected);
}
}
@@ -139,12 +276,12 @@ namespace CryptoExchange.Net.UnitTests
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);
RateLimitEvent? evnt = null;
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);
bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
Assert.That(expected);
}
}
@@ -163,7 +300,7 @@ namespace CryptoExchange.Net.UnitTests
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
RateLimitEvent? evnt = 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);
@@ -182,7 +319,7 @@ namespace CryptoExchange.Net.UnitTests
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
RateLimitEvent? evnt = null;
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);
@@ -202,7 +339,7 @@ namespace CryptoExchange.Net.UnitTests
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
RateLimitEvent? evnt = null;
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);
@@ -219,7 +356,7 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
RateLimitEvent? evnt = null;
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);
@@ -234,7 +371,7 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
RateLimitEvent? evnt = null;
RateLimitEvent evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
@@ -242,84 +379,5 @@ namespace CryptoExchange.Net.UnitTests
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);
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
}
[Test]
public async Task RateLimiterReset_Should_AllowNextRequestForSameDefinition()
{
// arrange
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 };
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);
// assert
Assert.That(evnt, Is.Null);
}
[Test]
public async Task RateLimiterReset_Should_NotAllowNextRequestForDifferentDefinition()
{
// arrange
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 };
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);
// assert
Assert.That(evnt, Is.Not.Null);
}
[TestCase(null, null, true)]
[TestCase("Group1", null, false)]
[TestCase(null, "Group2", false)]
[TestCase("Group1", "Group2", false)]
[TestCase("Group3", "Group3", true)]
public async Task RateLimiterWithDifferentGroups_Should_LimitPerGroup(string? group1, string? group2, bool expectLimited)
{
// arrange
var data = JsonSerializer.Serialize(new TestObject { });
var client1 = new TestRestClient(x =>
{
x.RateLimitGroup = group1;
});
client1.ApiClient1.SetNextResponse(data, System.Net.HttpStatusCode.OK);
var client2 = new TestRestClient(x =>
{
x.RateLimitGroup = group2;
});
client2.ApiClient1.SetNextResponse(data, System.Net.HttpStatusCode.OK);
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Request), 1, TimeSpan.FromSeconds(2), RateLimitWindowType.Fixed));
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
// act
var result1 = await client1.ApiClient1.GetResponseAsync<TestObject>(rateLimitGate: rateLimiter);
var result2 = await client2.ApiClient1.GetResponseAsync<TestObject>(rateLimitGate: rateLimiter);
// assert
Assert.That(evnt != null, Is.EqualTo(expectLimited));
}
}
}
@@ -1,632 +0,0 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class SharedQuantityTests
{
[Test]
public void SharedQuantityReference_IsZero_AllNull_Should_ReturnTrue()
{
// arrange
var quantity = new SharedOrderQuantity(null, null, null);
// act & assert
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantityReference_IsZero_AllZero_Should_ReturnTrue()
{
// arrange
var quantity = new SharedOrderQuantity(0, 0, 0);
// act & assert
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantityReference_IsZero_BaseAssetSet_Should_ReturnFalse()
{
// arrange
var quantity = new SharedOrderQuantity(1.5m, null, null);
// act & assert
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedQuantityReference_IsZero_QuoteAssetSet_Should_ReturnFalse()
{
// arrange
var quantity = new SharedOrderQuantity(null, 100m, null);
// act & assert
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedQuantityReference_IsZero_ContractsSet_Should_ReturnFalse()
{
// arrange
var quantity = new SharedOrderQuantity(null, null, 10m);
// act & assert
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedQuantityReference_IsZero_NegativeValue_Should_ReturnTrue()
{
// arrange
var quantity = new SharedOrderQuantity(-1m, 0, 0);
// act & assert
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_DefaultConstructor_Should_SetAllPropertiesToNull()
{
// arrange & act
var quantity = new SharedQuantity();
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_Base_Should_SetBaseAssetQuantity()
{
// arrange
var expectedQuantity = 1.5m;
// act
var quantity = SharedQuantity.Base(expectedQuantity);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedQuantity));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_Base_WithZero_Should_SetZeroQuantity()
{
// arrange & act
var quantity = SharedQuantity.Base(0m);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(0m));
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_Base_WithLargeValue_Should_SetCorrectly()
{
// arrange
var largeValue = 999999.123456789m;
// act
var quantity = SharedQuantity.Base(largeValue);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(largeValue));
}
[Test]
public void SharedQuantity_Quote_Should_SetQuoteAssetQuantity()
{
// arrange
var expectedQuantity = 100m;
// act
var quantity = SharedQuantity.Quote(expectedQuantity);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(expectedQuantity));
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_Quote_WithDecimal_Should_PreserveDecimals()
{
// arrange
var expectedQuantity = 50.123456m;
// act
var quantity = SharedQuantity.Quote(expectedQuantity);
// assert
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(expectedQuantity));
}
[Test]
public void SharedQuantity_Contracts_Should_SetContractQuantity()
{
// arrange
var expectedQuantity = 10m;
// act
var quantity = SharedQuantity.Contracts(expectedQuantity);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.EqualTo(expectedQuantity));
}
[Test]
public void SharedQuantity_Contracts_WithFractionalValue_Should_SetCorrectly()
{
// arrange
var expectedQuantity = 2.5m;
// act
var quantity = SharedQuantity.Contracts(expectedQuantity);
// assert
Assert.That(quantity.QuantityInContracts, Is.EqualTo(expectedQuantity));
}
[Test]
public void SharedQuantity_BaseFromQuote_Should_CalculateCorrectly()
{
// arrange
var quoteQuantity = 100m;
var price = 50m;
var expectedBase = 2m; // 100 / 50 = 2
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedBase));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_BaseFromQuote_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var quoteQuantity = 100m;
var price = 3m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, decimalPlaces);
// assert
// 100 / 3 = 33.333... should round to 33.33
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(33.33m));
}
[Test]
public void SharedQuantity_BaseFromQuote_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var quoteQuantity = 100m;
var price = 7m;
var lotSize = 0.1m;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, 8, lotSize);
// assert
// 100 / 7 = 14.285714... should adjust to nearest 0.1 = 14.3
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(14.3m));
}
[Test]
public void SharedQuantity_BaseFromQuote_WithHighPrecision_Should_HandleCorrectly()
{
// arrange
var quoteQuantity = 1000m;
var price = 0.00001m;
var decimalPlaces = 8;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, decimalPlaces);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_QuoteFromBase_Should_CalculateCorrectly()
{
// arrange
var baseQuantity = 2m;
var price = 50m;
var expectedQuote = 100m; // 2 * 50 = 100
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedQuote));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_QuoteFromBase_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var baseQuantity = 1.234567m;
var price = 10m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, decimalPlaces);
// assert
// 1.234567 * 10 = 12.34567 should round to 12.35
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(12.35m));
}
[Test]
public void SharedQuantity_QuoteFromBase_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var baseQuantity = 3.456m;
var price = 10m;
var lotSize = 1m;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, 8, lotSize);
// assert
// 3.456 * 10 = 34.56 should adjust to nearest 1 = 35
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(35m));
}
[Test]
public void SharedQuantity_QuoteFromBase_WithSmallValues_Should_HandleCorrectly()
{
// arrange
var baseQuantity = 0.001m;
var price = 0.1m;
var decimalPlaces = 8;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, decimalPlaces);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(0.0001m));
}
[Test]
public void SharedQuantity_ContractsFromBase_Should_CalculateCorrectly()
{
// arrange
var baseQuantity = 100m;
var contractSize = 10m;
var expectedContracts = 10m; // 100 / 10 = 10
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedContracts));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_ContractsFromBase_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var baseQuantity = 100m;
var contractSize = 3m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize, decimalPlaces);
// assert
// 100 / 3 = 33.333... should round to 33.33
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(33.33m));
}
[Test]
public void SharedQuantity_ContractsFromBase_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var baseQuantity = 100m;
var contractSize = 7m;
var lotSize = 0.5m;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize, 8, lotSize);
// assert
// 100 / 7 = 14.285714... should adjust to nearest 0.5 = 14.5
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(14.5m));
}
[Test]
public void SharedQuantity_ContractsFromBase_WithFractionalContract_Should_HandleCorrectly()
{
// arrange
var baseQuantity = 1m;
var contractSize = 0.1m;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(10m));
}
[Test]
public void SharedQuantity_ContractsFromQuote_Should_CalculateCorrectly()
{
// arrange
var quoteQuantity = 1000m;
var contractSize = 10m;
var price = 50m;
var expectedContracts = 2m; // 1000 / 50 / 10 = 2
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedContracts));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var quoteQuantity = 100m;
var contractSize = 3m;
var price = 7m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, decimalPlaces);
// assert
// 100 / 7 / 3 = 4.761904... should round to 4.76
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(4.76m));
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var quoteQuantity = 1000m;
var contractSize = 7m;
var price = 13m;
var lotSize = 0.5m;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, 8, lotSize);
// assert
// 1000 / 13 / 7 = 10.989... should adjust to nearest 0.5 = 11.0
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(11.0m));
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithComplexValues_Should_CalculateCorrectly()
{
// arrange
var quoteQuantity = 5000m;
var contractSize = 0.01m;
var price = 25000m;
var decimalPlaces = 4;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, decimalPlaces);
// assert
// 5000 / 25000 / 0.01 = 20
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(20m));
}
[Test]
public void SharedOrderQuantity_DefaultConstructor_Should_SetAllPropertiesToNull()
{
// arrange & act
var quantity = new SharedOrderQuantity();
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetBaseAsset()
{
// arrange
var baseAsset = 5m;
// act
var quantity = new SharedOrderQuantity(baseAssetQuantity: baseAsset);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(baseAsset));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetQuoteAsset()
{
// arrange
var quoteAsset = 100m;
// act
var quantity = new SharedOrderQuantity(quoteAssetQuantity: quoteAsset);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetContracts()
{
// arrange
var contracts = 10m;
// act
var quantity = new SharedOrderQuantity(contractQuantity: contracts);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.EqualTo(contracts));
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetAllValues()
{
// arrange
var baseAsset = 1m;
var quoteAsset = 50m;
var contracts = 5m;
// act
var quantity = new SharedOrderQuantity(baseAsset, quoteAsset, contracts);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(baseAsset));
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(quantity.QuantityInContracts, Is.EqualTo(contracts));
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_WithNullValues_Should_HandleCorrectly()
{
// arrange & act
var quantity = new SharedOrderQuantity(null, null, null);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_RecordEquality_SameValues_Should_BeEqual()
{
// arrange
var quantity1 = SharedQuantity.Base(10m);
var quantity2 = SharedQuantity.Base(10m);
// act & assert
Assert.That(quantity1, Is.EqualTo(quantity2));
}
[Test]
public void SharedQuantity_RecordEquality_DifferentValues_Should_NotBeEqual()
{
// arrange
var quantity1 = SharedQuantity.Base(10m);
var quantity2 = SharedQuantity.Base(20m);
// act & assert
Assert.That(quantity1, Is.Not.EqualTo(quantity2));
}
[Test]
public void SharedQuantity_RecordEquality_DifferentTypes_Should_NotBeEqual()
{
// arrange
var quantity1 = SharedQuantity.Base(10m);
var quantity2 = SharedQuantity.Quote(10m);
// act & assert
Assert.That(quantity1, Is.Not.EqualTo(quantity2));
}
[Test]
public void SharedOrderQuantity_RecordEquality_SameValues_Should_BeEqual()
{
// arrange
var quantity1 = new SharedOrderQuantity(5m, 100m, 2m);
var quantity2 = new SharedOrderQuantity(5m, 100m, 2m);
// act & assert
Assert.That(quantity1, Is.EqualTo(quantity2));
}
[Test]
public void SharedQuantity_BaseFromQuote_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var quoteQuantity = 100m;
var price = 3m;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price);
// assert
// Default decimalPlaces = 8, default lotSize = 0.00000001
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_QuoteFromBase_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var baseQuantity = 1.234567m;
var price = 10m;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_ContractsFromBase_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var baseQuantity = 100m;
var contractSize = 3m;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var quoteQuantity = 1000m;
var contractSize = 10m;
var price = 50m;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
}
}
@@ -1,400 +0,0 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class SharedSymbolTests
{
[Test]
public void SharedSymbol_Constructor_Should_SetAllProperties()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "BTC";
var quoteAsset = "USDT";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(symbol.DeliverTime, Is.Null);
Assert.That(symbol.SymbolName, Is.Null);
}
[Test]
public void SharedSymbol_Constructor_WithDeliveryTime_Should_SetDeliveryTime()
{
// arrange
var tradingMode = TradingMode.DeliveryLinear;
var baseAsset = "BTC";
var quoteAsset = "USDT";
var deliveryTime = new DateTime(2026, 6, 25, 0, 0, 0, DateTimeKind.Utc);
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliveryTime);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(symbol.DeliverTime, Is.EqualTo(deliveryTime));
Assert.That(symbol.SymbolName, Is.Null);
}
[Test]
public void SharedSymbol_Constructor_WithNullDeliveryTime_Should_SetToNull()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "ETH";
var quoteAsset = "BTC";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime: null);
// assert
Assert.That(symbol.DeliverTime, Is.Null);
}
[TestCase(TradingMode.Spot)]
[TestCase(TradingMode.PerpetualLinear)]
[TestCase(TradingMode.PerpetualInverse)]
[TestCase(TradingMode.DeliveryLinear)]
[TestCase(TradingMode.DeliveryInverse)]
public void SharedSymbol_Constructor_WithDifferentTradingModes_Should_SetCorrectly(TradingMode tradingMode)
{
// arrange
var baseAsset = "BTC";
var quoteAsset = "USDT";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
}
[Test]
public void SharedSymbol_ConstructorWithSymbolName_Should_SetSymbolName()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "BTC";
var quoteAsset = "USDT";
var symbolName = "BTC-USDT";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(symbol.SymbolName, Is.EqualTo(symbolName));
Assert.That(symbol.DeliverTime, Is.Null);
}
[Test]
public void SharedSymbol_ConstructorWithSymbolName_WithCustomFormat_Should_SetCorrectly()
{
// arrange
var tradingMode = TradingMode.PerpetualLinear;
var baseAsset = "ETH";
var quoteAsset = "USDT";
var symbolName = "ETHUSDT-PERP";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
// assert
Assert.That(symbol.SymbolName, Is.EqualTo(symbolName));
}
[Test]
public void SharedSymbol_ConstructorWithSymbolName_WithEmptyString_Should_SetEmptyString()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "BTC";
var quoteAsset = "USDT";
var symbolName = "";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
// assert
Assert.That(symbol.SymbolName, Is.EqualTo(""));
}
[Test]
public void GetSymbol_WithSymbolNameSet_Should_ReturnSymbolName()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "CUSTOM-BTC-USDT");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("CUSTOM-BTC-USDT"));
}
[Test]
public void GetSymbol_WithSymbolNameNull_Should_UseFormatFunction()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}/{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTC/USDT"));
}
[Test]
public void GetSymbol_WithComplexFormatFunction_Should_ApplyCorrectly()
{
// arrange
var symbol = new SharedSymbol(TradingMode.PerpetualLinear, "ETH", "USDT");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => t == TradingMode.PerpetualLinear ? $"{b}{q}-PERP" : $"{b}{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("ETHUSDT-PERP"));
}
[Test]
public void GetSymbol_WithDeliveryTime_Should_PassDeliveryTimeToFormatter()
{
// arrange
var deliveryTime = new DateTime(2026, 6, 25);
var symbol = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime);
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => d.HasValue ? $"{b}{q}_{d.Value:yyyyMMdd}" : $"{b}{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTCUSDT_20260625"));
}
[Test]
public void GetSymbol_WithTradingMode_Should_PassTradingModeToFormatter()
{
// arrange
var symbol = new SharedSymbol(TradingMode.PerpetualInverse, "BTC", "USD");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) =>
{
return t switch
{
TradingMode.Spot => $"{b}{q}",
TradingMode.PerpetualLinear => $"{b}{q}-PERP",
TradingMode.PerpetualInverse => $"{b}{q}I-PERP",
_ => $"{b}{q}"
};
});
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTCUSDI-PERP"));
}
[Test]
public void GetSymbol_WithEmptySymbolName_Should_UseFormatFunction()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}-{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTC-USDT"));
}
[Test]
public void GetSymbol_WithWhitespaceSymbolName_Should_ReturnWhitespace()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", " ");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}-{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo(" "));
}
[Test]
public void SharedSymbol_RecordEquality_SameValues_Should_BeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act & assert
Assert.That(symbol1, Is.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentBaseAsset_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentQuoteAsset_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "EUR");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentTradingMode_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentDeliveryTime_Should_NotBeEqual()
{
// arrange
var deliveryTime1 = new DateTime(2026, 6, 25);
var deliveryTime2 = new DateTime(2026, 9, 25);
var symbol1 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime1);
var symbol2 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime2);
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentSymbolName_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTCUSDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTC-USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_OneWithSymbolNameOneWithout_Should_NotBeEqual()
{
// NOTE; although this should probably be equal it's considered not because the SymbolName property isn't equal
// Overridding equality to ignore SymbolName would be possible but would break the default record equality behavior and cause confusion
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTCUSDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_WithAllPropertiesSet_Should_BeEqual()
{
// arrange
var deliveryTime = new DateTime(2026, 6, 25);
var symbol1 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime)
{
SymbolName = "BTCUSDT-0625"
};
var symbol2 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime)
{
SymbolName = "BTCUSDT-0625"
};
// act & assert
Assert.That(symbol1, Is.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_Properties_Should_BeSettable()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
symbol.BaseAsset = "ETH";
symbol.QuoteAsset = "EUR";
symbol.TradingMode = TradingMode.PerpetualLinear;
symbol.SymbolName = "CUSTOM";
symbol.DeliverTime = DateTime.UtcNow;
// assert
Assert.That(symbol.BaseAsset, Is.EqualTo("ETH"));
Assert.That(symbol.QuoteAsset, Is.EqualTo("EUR"));
Assert.That(symbol.TradingMode, Is.EqualTo(TradingMode.PerpetualLinear));
Assert.That(symbol.SymbolName, Is.EqualTo("CUSTOM"));
Assert.That(symbol.DeliverTime, Is.Not.Null);
}
[Test]
public void SharedSymbol_WithSpecialCharactersInAssets_Should_HandleCorrectly()
{
// arrange
var baseAsset = "BTC-123";
var quoteAsset = "USDT_2.0";
// act
var symbol = new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset);
// assert
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
}
[Test]
public void SharedSymbol_WithLongAssetNames_Should_HandleCorrectly()
{
// arrange
var baseAsset = "VERYLONGASSETNAMEFORTESTING";
var quoteAsset = "ANOTHERVERYLONGASSETNAME";
// act
var symbol = new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset);
// assert
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
}
}
}
@@ -0,0 +1,231 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.UnitTests.TestImplementations;
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using NUnit.Framework.Legacy;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
public class SocketClientTests
{
[TestCase]
public void SettingOptions_Should_ResultInOptionsSet()
{
//arrange
//act
var client = new TestSocketClient(options =>
{
options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
options.SubOptions.MaxSocketConnections = 1;
});
//assert
ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
}
[TestCase(true)]
[TestCase(false)]
public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
{
//arrange
var client = new TestSocketClient();
var socket = client.CreateSocket();
socket.CanConnect = canConnect;
//act
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
//assert
Assert.That(connectResult.Success == canConnect);
}
[TestCase]
public void SocketMessages_Should_BeProcessedInDataHandlers()
{
// arrange
var client = new TestSocketClient(options => {
options.ReconnectInterval = TimeSpan.Zero;
});
var socket = client.CreateSocket();
socket.CanConnect = true;
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
var rstEvent = new ManualResetEvent(false);
Dictionary<string, string> result = null;
client.SubClient.ConnectSocketSub(sub);
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
{
result = messageEvent.Data;
rstEvent.Set();
});
sub.AddSubscription(subObj);
// act
socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
rstEvent.WaitOne(1000);
// assert
Assert.That(result["property"] == "123");
}
[TestCase(false)]
[TestCase(true)]
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
{
// arrange
var client = new TestSocketClient(options =>
{
options.ReconnectInterval = TimeSpan.Zero;
options.SubOptions.OutputOriginalData = enabled;
});
var socket = client.CreateSocket();
socket.CanConnect = true;
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
var rstEvent = new ManualResetEvent(false);
string original = null;
client.SubClient.ConnectSocketSub(sub);
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
{
original = messageEvent.OriginalData;
rstEvent.Set();
});
sub.AddSubscription(subObj);
var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" });
// act
socket.InvokeMessage(msgToSend);
rstEvent.WaitOne(1000);
// assert
Assert.That(original == (enabled ? msgToSend : null));
}
[TestCase()]
public void UnsubscribingStream_Should_CloseTheSocket()
{
// arrange
var client = new TestSocketClient(options =>
{
options.ReconnectInterval = TimeSpan.Zero;
});
var socket = client.CreateSocket();
socket.CanConnect = true;
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
client.SubClient.ConnectSocketSub(sub);
var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
var ups = new UpdateSubscription(sub, subscription);
sub.AddSubscription(subscription);
// act
client.UnsubscribeAsync(ups).Wait();
// assert
Assert.That(socket.Connected == false);
}
[TestCase()]
public void UnsubscribingAll_Should_CloseAllSockets()
{
// arrange
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
var socket1 = client.CreateSocket();
var socket2 = client.CreateSocket();
socket1.CanConnect = true;
socket2.CanConnect = true;
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket1, null);
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
client.SubClient.ConnectSocketSub(sub1);
client.SubClient.ConnectSocketSub(sub2);
var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
sub1.AddSubscription(subscription1);
sub2.AddSubscription(subscription2);
var ups1 = new UpdateSubscription(sub1, subscription1);
var ups2 = new UpdateSubscription(sub2, subscription2);
// act
client.UnsubscribeAllAsync().Wait();
// assert
Assert.That(socket1.Connected == false);
Assert.That(socket2.Connected == false);
}
[TestCase()]
public void FailingToConnectSocket_Should_ReturnError()
{
// arrange
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
var socket = client.CreateSocket();
socket.CanConnect = false;
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
// act
var connectResult = client.SubClient.ConnectSocketSub(sub1);
// assert
ClassicAssert.IsFalse(connectResult.Success);
}
[TestCase()]
public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
{
// arrange
var channel = "trade_btcusd";
var client = new TestSocketClient(opt =>
{
opt.OutputOriginalData = true;
opt.SocketSubscriptionsCombineTarget = 1;
});
var socket = client.CreateSocket();
socket.CanConnect = true;
client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
// act
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" }));
await sub;
// assert
ClassicAssert.IsFalse(client.SubClient.TestSubscription.Confirmed);
}
[TestCase()]
public async Task SuccessResponse_Should_ConfirmSubscription()
{
// arrange
var channel = "trade_btcusd";
var client = new TestSocketClient(opt =>
{
opt.OutputOriginalData = true;
opt.SocketSubscriptionsCombineTarget = 1;
});
var socket = client.CreateSocket();
socket.CanConnect = true;
client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
// act
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" }));
await sub;
// assert
Assert.That(client.SubClient.TestSubscription.Confirmed);
}
}
}
@@ -1,235 +0,0 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using NUnit.Framework;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
[TestFixture]
public class QueryRouterTests
{
[Test]
public void BuildFromRoutes_Should_GroupRoutesByTypeIdentifier_AndSetDeserializationType()
{
// arrange
var routes = new MessageRoute[]
{
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null),
MessageRoute<int>.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null)
};
var router = new QueryRouter(routes);
// act
var type1Routes = router.GetRoutes("type1");
var type2Routes = router.GetRoutes("type2");
var missingRoutes = router.GetRoutes("missing");
// assert
Assert.That(type1Routes, Is.Not.Null);
Assert.That(type2Routes, Is.Not.Null);
Assert.That(missingRoutes, Is.Null);
Assert.That(type1Routes, Is.TypeOf<QueryRouteCollection>());
Assert.That(type2Routes, Is.TypeOf<QueryRouteCollection>());
Assert.That(type1Routes!.DeserializationType, Is.EqualTo(typeof(string)));
Assert.That(type2Routes!.DeserializationType, Is.EqualTo(typeof(int)));
}
[Test]
public void AddRoute_Should_SetMultipleReaders_WhenAnyRouteAllowsMultipleReaders()
{
// arrange
var collection = new QueryRouteCollection(typeof(string));
// act
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) => null));
var beforeMultipleReaders = collection.MultipleReaders;
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) => null, true));
var afterMultipleReaders = collection.MultipleReaders;
// assert
Assert.That(beforeMultipleReaders, Is.False);
Assert.That(afterMultipleReaders, Is.True);
}
[Test]
public void Handle_Should_InvokeRoutesWithoutTopicFilter_WhenTopicFilterIsNull()
{
// arrange
var calls = new List<string>();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
}));
collection.Build();
// act
var handled = collection.Handle(null, null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.Null);
Assert.That(calls, Is.EqualTo(new[] { "no-topic" }));
}
[Test]
public void Handle_Should_ReturnFalse_WhenNoRoutesMatch()
{
// arrange
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("other-topic", MessageRoute<string>.CreateWithTopicFilter("type", "other-topic", (_, _, _, _) => null));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.False);
Assert.That(result, Is.Null);
}
[Test]
public void Handle_Should_InvokeRoutesWithoutTopicFilter_AndMatchingTopicRoutes()
{
// arrange
var calls = new List<string>();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.Null);
Assert.That(calls, Is.EqualTo(new[] { "no-topic", "topic" }));
}
[Test]
public void Handle_Should_StopAfterFirstNonNullMatchingResult_WhenMultipleReadersIsFalse()
{
// arrange
var calls = new List<string>();
var expectedResult = CallResult.SuccessResult;
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return expectedResult;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return new CallResult(null);
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(expectedResult));
Assert.That(calls, Is.EqualTo(new[] { "first" }));
}
[Test]
public void Handle_Should_ContinueAfterNonNullMatchingResult_WhenMultipleReadersIsTrue()
{
// arrange
var calls = new List<string>();
var expectedResult = CallResult.SuccessResult;
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return expectedResult;
}, true));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return new CallResult(null);
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(expectedResult));
Assert.That(calls, Is.EqualTo(new[] { "first", "second" }));
}
[Test]
public void Handle_Should_ContinueUntilNonNullResult_WhenEarlierMatchingRoutesReturnNull()
{
// arrange
var calls = new List<string>();
var expectedResult = CallResult.SuccessResult;
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return expectedResult;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("third");
return new CallResult(null);
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(expectedResult));
Assert.That(calls, Is.EqualTo(new[] { "first", "second" }));
}
[Test]
public void Handle_Should_ReturnHandledTrue_WhenMatchingRoutesReturnNull()
{
// arrange
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) => null));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.Null);
}
}
}
@@ -1,167 +0,0 @@
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using CryptoExchange.Net.Sockets.Interfaces;
using NUnit.Framework;
using System;
using System.Linq;
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
[TestFixture]
public class RoutingTableTests
{
[Test]
public void Update_Should_CreateEntriesPerTypeIdentifier_WithCorrectDeserializationTypeAndHandlers()
{
// arrange
var processor1 = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null)));
var processor2 = new TestMessageProcessor(
2,
MessageRouter.Create(
MessageRoute<int>.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null)));
var table = new RoutingTable();
// act
table.Update(new IMessageProcessor[] { processor1, processor2 });
var type1Entry = table.GetRouteTableEntry("type1");
var type2Entry = table.GetRouteTableEntry("type2");
var missingEntry = table.GetRouteTableEntry("missing");
// assert
Assert.That(type1Entry, Is.Not.Null);
Assert.That(type2Entry, Is.Not.Null);
Assert.That(missingEntry, Is.Null);
Assert.That(type1Entry!.DeserializationType, Is.EqualTo(typeof(string)));
Assert.That(type1Entry.IsStringOutput, Is.True);
Assert.That(type1Entry.Handlers, Has.Count.EqualTo(1));
Assert.That(type1Entry.Handlers.Single(), Is.SameAs(processor1));
Assert.That(type2Entry!.DeserializationType, Is.EqualTo(typeof(int)));
Assert.That(type2Entry.IsStringOutput, Is.False);
Assert.That(type2Entry.Handlers, Has.Count.EqualTo(1));
Assert.That(type2Entry.Handlers.Single(), Is.SameAs(processor2));
}
[Test]
public void Update_Should_AddMultipleProcessors_ForSameTypeIdentifier()
{
// arrange
var processor1 = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
var processor2 = new TestMessageProcessor(
2,
MessageRouter.Create(
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null)));
var table = new RoutingTable();
// act
table.Update(new IMessageProcessor[] { processor1, processor2 });
var entry = table.GetRouteTableEntry("type1");
// assert
Assert.That(entry, Is.Not.Null);
Assert.That(entry!.DeserializationType, Is.EqualTo(typeof(string)));
Assert.That(entry.Handlers, Has.Count.EqualTo(2));
Assert.That(entry.Handlers, Does.Contain(processor1));
Assert.That(entry.Handlers, Does.Contain(processor2));
}
[Test]
public void Update_Should_ReplacePreviousEntries()
{
// arrange
var initialProcessor = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
var replacementProcessor = new TestMessageProcessor(
2,
MessageRouter.Create(
MessageRoute<int>.CreateWithoutTopicFilter("type2", (_, _, _, _) => null)));
var table = new RoutingTable();
table.Update(new IMessageProcessor[] { initialProcessor });
// act
table.Update(new IMessageProcessor[] { replacementProcessor });
var oldEntry = table.GetRouteTableEntry("type1");
var newEntry = table.GetRouteTableEntry("type2");
// assert
Assert.That(oldEntry, Is.Null);
Assert.That(newEntry, Is.Not.Null);
Assert.That(newEntry!.DeserializationType, Is.EqualTo(typeof(int)));
Assert.That(newEntry.Handlers, Has.Count.EqualTo(1));
Assert.That(newEntry.Handlers.Single(), Is.SameAs(replacementProcessor));
}
[Test]
public void Update_WithEmptyProcessors_Should_ClearEntries()
{
// arrange
var processor = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
var table = new RoutingTable();
table.Update(new IMessageProcessor[] { processor });
// act
table.Update(Array.Empty<IMessageProcessor>());
// assert
Assert.That(table.GetRouteTableEntry("type1"), Is.Null);
}
[Test]
public void TypeRoutingCollection_Should_SetIsStringOutput_BasedOnDeserializationType()
{
// arrange & act
var stringCollection = new TypeRoutingCollection(typeof(string));
var intCollection = new TypeRoutingCollection(typeof(int));
// assert
Assert.That(stringCollection.IsStringOutput, Is.True);
Assert.That(stringCollection.DeserializationType, Is.EqualTo(typeof(string)));
Assert.That(stringCollection.Handlers, Is.Empty);
Assert.That(intCollection.IsStringOutput, Is.False);
Assert.That(intCollection.DeserializationType, Is.EqualTo(typeof(int)));
Assert.That(intCollection.Handlers, Is.Empty);
}
private sealed class TestMessageProcessor : IMessageProcessor
{
public int Id { get; }
public MessageRouter MessageRouter { get; }
public TestMessageProcessor(int id, MessageRouter messageRouter)
{
Id = id;
MessageRouter = messageRouter;
}
public event Action? OnMessageRouterUpdated;
public bool Handle(string typeIdentifier, string? topicFilter, SocketConnection socketConnection, DateTime receiveTime, string? originalData, object result)
{
return true;
}
}
}
}
@@ -1,160 +0,0 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets.Default.Routing;
using NUnit.Framework;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
[TestFixture]
public class SubscriptionRouterTests
{
[Test]
public void BuildFromRoutes_Should_GroupRoutesByTypeIdentifier_AndSetDeserializationType()
{
// arrange
var routes = new MessageRoute[]
{
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null),
MessageRoute<int>.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null)
};
var router = new SubscriptionRouter(routes);
// act
var type1Routes = router.GetRoutes("type1");
var type2Routes = router.GetRoutes("type2");
var missingRoutes = router.GetRoutes("missing");
// assert
Assert.That(type1Routes, Is.Not.Null);
Assert.That(type2Routes, Is.Not.Null);
Assert.That(missingRoutes, Is.Null);
Assert.That(type1Routes, Is.TypeOf<SubscriptionRouteCollection>());
Assert.That(type2Routes, Is.TypeOf<SubscriptionRouteCollection>());
Assert.That(type1Routes!.DeserializationType, Is.EqualTo(typeof(string)));
Assert.That(type2Routes!.DeserializationType, Is.EqualTo(typeof(int)));
}
[Test]
public void Handle_Should_InvokeRoutesWithoutTopicFilter_WhenTopicFilterIsNull()
{
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
}));
collection.Build();
// act
var handled = collection.Handle(null, null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(calls, Is.EqualTo(new[] { "no-topic" }));
}
[Test]
public void Handle_Should_ReturnFalse_WhenNoRoutesMatch()
{
// arrange
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute("other-topic", MessageRoute<string>.CreateWithTopicFilter("type", "other-topic", (_, _, _, _) => null));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.False);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
}
[Test]
public void Handle_Should_InvokeRoutesWithoutTopicFilter_AndMatchingTopicRoutes()
{
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(calls, Is.EqualTo(new[] { "no-topic", "topic" }));
}
[Test]
public void Handle_Should_InvokeAllMatchingTopicRoutes()
{
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return CallResult.SuccessResult;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return null;
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(calls, Is.EqualTo(new[] { "first", "second" }));
}
[Test]
public void Handle_Should_NotInvokeTopicRoutes_WhenTopicFilterIsNull()
{
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
}));
collection.Build();
// act
var handled = collection.Handle(null, null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.False);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(calls, Is.Empty);
}
}
}
@@ -0,0 +1,442 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters.SystemTextJson;
using System.Text.Json;
using NUnit.Framework;
using System;
using System.Text.Json.Serialization;
using NUnit.Framework.Legacy;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Testing.Comparers;
using CryptoExchange.Net.SharedApis;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class SystemTextJsonConverterTests
{
[TestCase("2021-05-12")]
[TestCase("20210512")]
[TestCase("210512")]
[TestCase("1620777600.000")]
[TestCase("1620777600000")]
[TestCase("2021-05-12T00:00:00.000Z")]
[TestCase("2021-05-12T00:00:00.000000000Z")]
[TestCase("0.000000", true)]
[TestCase("0", true)]
[TestCase("", true)]
[TestCase(" ", true)]
public void TestDateTimeConverterString(string input, bool expectNull = false)
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": \"{input}\" }}");
Assert.That(output.Time == (expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
}
[TestCase(1620777600.000)]
[TestCase(1620777600000d)]
public void TestDateTimeConverterDouble(double input)
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
Assert.That(output.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[TestCase(1620777600)]
[TestCase(1620777600000)]
[TestCase(1620777600000000)]
[TestCase(1620777600000000000)]
[TestCase(0, true)]
public void TestDateTimeConverterLong(long input, bool expectNull = false)
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
Assert.That(output.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
}
[TestCase(1620777600)]
[TestCase(1620777600.000)]
public void TestDateTimeConverterFromSeconds(double input)
{
var output = DateTimeConverter.ConvertFromSeconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToSeconds()
{
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600);
}
[TestCase(1620777600000)]
[TestCase(1620777600000.000)]
public void TestDateTimeConverterFromMilliseconds(double input)
{
var output = DateTimeConverter.ConvertFromMilliseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToMilliseconds()
{
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000);
}
[TestCase(1620777600000000)]
public void TestDateTimeConverterFromMicroseconds(long input)
{
var output = DateTimeConverter.ConvertFromMicroseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToMicroseconds()
{
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000000);
}
[TestCase(1620777600000000000)]
public void TestDateTimeConverterFromNanoseconds(long input)
{
var output = DateTimeConverter.ConvertFromNanoseconds(input);
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
}
[Test]
public void TestDateTimeConverterToNanoseconds()
{
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
Assert.That(output == 1620777600000000000);
}
[TestCase()]
public void TestDateTimeConverterNull()
{
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": null }}");
Assert.That(output.Time == null);
}
[TestCase(TestEnum.One, "1")]
[TestCase(TestEnum.Two, "2")]
[TestCase(TestEnum.Three, "three")]
[TestCase(TestEnum.Four, "Four")]
[TestCase(null, null)]
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
{
var output = EnumConverter.GetString(value);
Assert.That(output == expected);
}
[TestCase(TestEnum.One, "1")]
[TestCase(TestEnum.Two, "2")]
[TestCase(TestEnum.Three, "three")]
[TestCase(TestEnum.Four, "Four")]
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
{
var output = EnumConverter.GetString(value);
Assert.That(output == expected);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", null)]
[TestCase(null, null)]
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
Assert.That(output.Value == expected);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", TestEnum.One)]
[TestCase(null, TestEnum.One)]
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<NotNullableSTJEnumObject>($"{{ \"Value\": {val} }}");
Assert.That(output.Value == expected);
}
[TestCase("1", TestEnum.One)]
[TestCase("2", TestEnum.Two)]
[TestCase("3", TestEnum.Three)]
[TestCase("three", TestEnum.Three)]
[TestCase("Four", TestEnum.Four)]
[TestCase("four", TestEnum.Four)]
[TestCase("Four1", null)]
[TestCase(null, null)]
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
{
var result = EnumConverter.ParseString<TestEnum>(value);
Assert.That(result == expected);
}
[TestCase("1", true)]
[TestCase("true", true)]
[TestCase("yes", true)]
[TestCase("y", true)]
[TestCase("on", true)]
[TestCase("-1", false)]
[TestCase("0", false)]
[TestCase("n", false)]
[TestCase("no", false)]
[TestCase("false", false)]
[TestCase("off", false)]
[TestCase("", null)]
public void TestBoolConverter(string value, bool? expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
Assert.That(output.Value == expected);
}
[TestCase("1", true)]
[TestCase("true", true)]
[TestCase("yes", true)]
[TestCase("y", true)]
[TestCase("on", true)]
[TestCase("-1", false)]
[TestCase("0", false)]
[TestCase("n", false)]
[TestCase("no", false)]
[TestCase("false", false)]
[TestCase("off", false)]
[TestCase("", false)]
public void TestBoolConverterNotNullable(string value, bool expected)
{
var val = value == null ? "null" : $"\"{value}\"";
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
Assert.That(output.Value == expected);
}
[TestCase("1", 1)]
[TestCase("1.1", 1.1)]
[TestCase("-1.1", -1.1)]
[TestCase(null, null)]
[TestCase("", null)]
[TestCase("null", null)]
[TestCase("nan", null)]
[TestCase("1E+2", 100)]
[TestCase("1E-2", 0.01)]
[TestCase("Infinity", 999)] // 999 is workaround for not being able to specify decimal.MinValue
[TestCase("-Infinity", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
[TestCase("80228162514264337593543950335", 999)] // 999 is workaround for not being able to specify decimal.MaxValue
[TestCase("-80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
public void TestDecimalConverterString(string value, decimal? expected)
{
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \""+ value + "\"}");
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MinValue : expected == 999 ? decimal.MaxValue: expected));
}
[TestCase("1", 1)]
[TestCase("1.1", 1.1)]
[TestCase("-1.1", -1.1)]
[TestCase("null", null)]
[TestCase("1E+2", 100)]
[TestCase("1E-2", 0.01)]
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
public void TestDecimalConverterNumber(string value, decimal? expected)
{
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": " + value + "}");
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
}
[Test()]
public void TestArrayConverter()
{
var data = new Test()
{
Prop1 = 2,
Prop2 = null,
Prop3 = "123",
Prop3Again = "123",
Prop4 = null,
Prop5 = new Test2
{
Prop21 = 3,
Prop22 = "456"
},
Prop6 = new Test3
{
Prop31 = 4,
Prop32 = "789"
},
Prop7 = TestEnum.Two,
TestInternal = new Test
{
Prop1 = 10
},
Prop8 = new Test3
{
Prop31 = 5,
Prop32 = "101"
},
};
var options = new JsonSerializerOptions()
{
TypeInfoResolver = new SerializationContext()
};
var serialized = JsonSerializer.Serialize(data);
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
Assert.That(deserialized.Prop1, Is.EqualTo(2));
Assert.That(deserialized.Prop2, Is.Null);
Assert.That(deserialized.Prop3, Is.EqualTo("123"));
Assert.That(deserialized.Prop3Again, Is.EqualTo("123"));
Assert.That(deserialized.Prop4, Is.Null);
Assert.That(deserialized.Prop5.Prop21, Is.EqualTo(3));
Assert.That(deserialized.Prop5.Prop22, Is.EqualTo("456"));
Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4));
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
Assert.That(deserialized.TestInternal.Prop1, Is.EqualTo(10));
Assert.That(deserialized.Prop8.Prop31, Is.EqualTo(5));
Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101"));
}
[TestCase(TradingMode.Spot, "ETH", "USDT", null)]
[TestCase(TradingMode.PerpetualLinear, "ETH", "USDT", null)]
[TestCase(TradingMode.DeliveryLinear, "ETH", "USDT", 1748432430)]
public void TestSharedSymbolConversion(TradingMode tradingMode, string baseAsset, string quoteAsset, int? deliverTime)
{
DateTime? time = deliverTime == null ? null : DateTimeConverter.ParseFromDouble(deliverTime.Value);
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, time);
var serialized = JsonSerializer.Serialize(symbol);
var restored = JsonSerializer.Deserialize<SharedSymbol>(serialized);
Assert.That(restored.TradingMode, Is.EqualTo(symbol.TradingMode));
Assert.That(restored.BaseAsset, Is.EqualTo(symbol.BaseAsset));
Assert.That(restored.QuoteAsset, Is.EqualTo(symbol.QuoteAsset));
Assert.That(restored.DeliverTime, Is.EqualTo(symbol.DeliverTime));
}
[TestCase(0.1, null, null)]
[TestCase(0.1, 0.1, null)]
[TestCase(0.1, 0.1, 0.1)]
[TestCase(null, 0.1, null)]
[TestCase(null, 0.1, 0.1)]
public void TestSharedQuantityConversion(double? baseQuantity, double? quoteQuantity, double? contractQuantity)
{
var symbol = new SharedOrderQuantity((decimal?)baseQuantity, (decimal?)quoteQuantity, (decimal?)contractQuantity);
var serialized = JsonSerializer.Serialize(symbol);
var restored = JsonSerializer.Deserialize<SharedOrderQuantity>(serialized);
Assert.That(restored.QuantityInBaseAsset, Is.EqualTo(symbol.QuantityInBaseAsset));
Assert.That(restored.QuantityInQuoteAsset, Is.EqualTo(symbol.QuantityInQuoteAsset));
Assert.That(restored.QuantityInContracts, Is.EqualTo(symbol.QuantityInContracts));
}
}
public class STJDecimalObject
{
[JsonConverter(typeof(DecimalConverter))]
[JsonPropertyName("test")]
public decimal? Test { get; set; }
}
public class STJTimeObject
{
[JsonConverter(typeof(DateTimeConverter))]
[JsonPropertyName("time")]
public DateTime? Time { get; set; }
}
public class STJEnumObject
{
public TestEnum? Value { get; set; }
}
public class NotNullableSTJEnumObject
{
public TestEnum Value { get; set; }
}
public class STJBoolObject
{
public bool? Value { get; set; }
}
public class NotNullableSTJBoolObject
{
public bool Value { get; set; }
}
[JsonConverter(typeof(ArrayConverter<Test>))]
record Test
{
[ArrayProperty(0)]
public int Prop1 { get; set; }
[ArrayProperty(1)]
public int? Prop2 { get; set; }
[ArrayProperty(2)]
public string Prop3 { get; set; }
[ArrayProperty(2)]
public string Prop3Again { get; set; }
[ArrayProperty(3)]
public string Prop4 { get; set; }
[ArrayProperty(4)]
public Test2 Prop5 { get; set; }
[ArrayProperty(5)]
public Test3 Prop6 { get; set; }
[ArrayProperty(6), JsonConverter(typeof(EnumConverter<TestEnum>))]
public TestEnum? Prop7 { get; set; }
[ArrayProperty(7)]
public Test TestInternal { get; set; }
[ArrayProperty(8), JsonConversion]
public Test3 Prop8 { get; set; }
}
[JsonConverter(typeof(ArrayConverter<Test2>))]
record Test2
{
[ArrayProperty(0)]
public int Prop21 { get; set; }
[ArrayProperty(1)]
public string Prop22 { get; set; }
}
record Test3
{
[JsonPropertyName("prop31")]
public int Prop31 { get; set; }
[JsonPropertyName("prop32")]
public string Prop32 { get; set; }
}
[JsonConverter(typeof(EnumConverter<TestEnum>))]
public enum TestEnum
{
[Map("1")]
One,
[Map("2")]
Two,
[Map("three", "3")]
Three,
Four
}
[JsonSerializable(typeof(Test))]
[JsonSerializable(typeof(Test2))]
[JsonSerializable(typeof(Test3))]
[JsonSerializable(typeof(NotNullableSTJBoolObject))]
[JsonSerializable(typeof(STJBoolObject))]
[JsonSerializable(typeof(NotNullableSTJEnumObject))]
[JsonSerializable(typeof(STJEnumObject))]
[JsonSerializable(typeof(STJDecimalObject))]
[JsonSerializable(typeof(STJTimeObject))]
internal partial class SerializationContext : JsonSerializerContext
{
}
}
@@ -0,0 +1,50 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class SubResponse
{
[JsonPropertyName("action")]
public string Action { get; set; } = null!;
[JsonPropertyName("channel")]
public string Channel { get; set; } = null!;
[JsonPropertyName("status")]
public string Status { get; set; } = null!;
}
internal class UnsubResponse
{
[JsonPropertyName("action")]
public string Action { get; set; } = null!;
[JsonPropertyName("status")]
public string Status { get; set; } = null!;
}
internal class TestChannelQuery : Query<SubResponse>
{
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
MessageMatcher = MessageMatcher.Create<SubResponse>(request + "-" + channel, HandleMessage);
}
public CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
{
if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase))
{
return new CallResult<SubResponse>(new ServerError(ErrorInfo.Unknown with { Message = message.Data.Status }));
}
return message.ToCallResult();
}
}
}
@@ -0,0 +1,17 @@
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class TestQuery : Query<object>
{
public TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
MessageMatcher = MessageMatcher.Create<object>(identifier);
}
}
}
@@ -0,0 +1,34 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class TestSubscription<T> : Subscription<object, object>
{
private readonly Action<DataEvent<T>> _handler;
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
{
_handler = handler;
MessageMatcher = MessageMatcher.Create<T>("update-topic", DoHandleMessage);
}
public CallResult DoHandleMessage(SocketConnection connection, DataEvent<T> message)
{
_handler.Invoke(message);
return new CallResult(null);
}
protected override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
protected override Query GetUnsubQuery(SocketConnection connection) => new TestQuery("unsub", new object(), false, 1);
}
}
@@ -0,0 +1,34 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging;
using Moq;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class TestSubscriptionWithResponseCheck<T> : Subscription<SubResponse, UnsubResponse>
{
private readonly Action<DataEvent<T>> _handler;
private readonly string _channel;
public TestSubscriptionWithResponseCheck(string channel, Action<DataEvent<T>> handler) : base(Mock.Of<ILogger>(), false)
{
MessageMatcher = MessageMatcher.Create<T>(channel, DoHandleMessage);
_handler = handler;
_channel = channel;
}
public CallResult DoHandleMessage(SocketConnection connection, DataEvent<T> message)
{
_handler.Invoke(message);
return new CallResult(null);
}
protected override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
protected override Query GetUnsubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "unsubscribe", false, 1);
}
}
@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace CryptoExchange.Net.UnitTests
{
public class TestBaseClient: BaseClient
{
public TestSubClient SubClient { get; }
public TestBaseClient(): base(null, "Test")
{
var options = new TestClientOptions();
_logger = NullLogger.Instance;
Initialize(options);
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
}
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
{
_logger = NullLogger.Instance;
Initialize(exchangeOptions);
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
}
public void Log(LogLevel verbosity, string data)
{
_logger.Log(verbosity, data);
}
}
public class TestSubClient : RestApiClient
{
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
{
}
public CallResult<T> Deserialize<T>(string data)
{
var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
var accessor = CreateAccessor();
var valid = accessor.Read(stream, true).Result;
if (!valid)
return new CallResult<T>(new ServerError(ErrorInfo.Unknown with { Message = data }));
var deserializeResult = accessor.Deserialize<T>();
return deserializeResult;
}
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public override TimeSpan? GetTimeOffset() => null;
public override TimeSyncInfo GetTimeSyncInfo() => null;
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
}
public class TestAuthProvider : AuthenticationProvider
{
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
{
}
public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig)
{
}
public string GetKey() => _credentials.Key;
public string GetSecret() => _credentials.Secret;
}
}
@@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestHelpers
{
[ExcludeFromCodeCoverage]
public static bool AreEqual<T>(T self, T to, params string[] ignore) where T : class
{
if (self != null && to != null)
{
var type = self.GetType();
var ignoreList = new List<string>(ignore);
foreach (var pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (ignoreList.Contains(pi.Name))
{
continue;
}
var selfValue = type.GetProperty(pi.Name).GetValue(self, null);
var toValue = type.GetProperty(pi.Name).GetValue(to, null);
if (pi.PropertyType.IsClass && !pi.PropertyType.Module.ScopeName.Equals("System.Private.CoreLib.dll"))
{
// Check of "CommonLanguageRuntimeLibrary" is needed because string is also a class
if (AreEqual(selfValue, toValue, ignore))
{
continue;
}
return false;
}
if (selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue)))
{
return false;
}
}
return true;
}
return self == to;
}
}
}
@@ -1,11 +1,11 @@
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.Implementations
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestObject
{
[JsonPropertyName("other")]
public string StringData { get; set; } = string.Empty;
public string StringData { get; set; }
[JsonPropertyName("intData")]
public int IntData { get; set; }
[JsonPropertyName("decimalData")]
@@ -0,0 +1,236 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using Moq;
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;
using System.Linq;
using CryptoExchange.Net.Converters.SystemTextJson;
using System.Text.Json.Serialization;
using CryptoExchange.Net.Objects.Errors;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestRestClient: BaseRestClient
{
public TestRestApi1Client Api1 { get; }
public TestRestApi2Client Api2 { get; }
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
{
}
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
{
Initialize(options.Value);
Api1 = new TestRestApi1Client(options.Value);
Api2 = new TestRestApi2Client(options.Value);
}
public void SetResponse(string responseData, out IRequest requestObj)
{
var expectedBytes = Encoding.UTF8.GetBytes(responseData);
var responseStream = new MemoryStream();
responseStream.Write(expectedBytes, 0, expectedBytes.Length);
responseStream.Seek(0, SeekOrigin.Begin);
var response = new Mock<IResponse>();
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
var headers = new Dictionary<string, string[]>();
var request = new Mock<IRequest>();
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray());
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
{
request.Setup(a => a.Uri).Returns(uri);
request.Setup(a => a.Method).Returns(method);
})
.Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
{
request.Setup(a => a.Uri).Returns(uri);
request.Setup(a => a.Method).Returns(method);
})
.Returns(request.Object);
requestObj = request.Object;
}
public void SetErrorWithoutResponse(HttpStatusCode code, string message)
{
var we = new HttpRequestException();
typeof(HttpRequestException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, message);
var request = new Mock<IRequest>();
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
request.Setup(c => c.GetHeaders()).Returns(new KeyValuePair<string, string[]>[0]);
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object);
}
public void SetErrorWithResponse(string responseData, HttpStatusCode code)
{
var expectedBytes = Encoding.UTF8.GetBytes(responseData);
var responseStream = new MemoryStream();
responseStream.Write(expectedBytes, 0, expectedBytes.Length);
responseStream.Seek(0, SeekOrigin.Begin);
var response = new Mock<IResponse>();
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
var headers = new List<KeyValuePair<string, string[]>>();
var request = new Mock<IRequest>();
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(new KeyValuePair<string, string[]>(key, new string[] { val })));
request.Setup(c => c.GetHeaders()).Returns(headers.ToArray());
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
.Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
.Returns(request.Object);
}
}
public class TestRestApi1Client : RestApiClient
{
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
{
RequestFactory = new Mock<IRequestFactory>().Object;
}
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions() { TypeInfoResolver = new TestSerializerContext() });
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
{
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
}
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, ParameterCollection parameters, Dictionary<string, string> headers) where T : class
{
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", method) { Weight = 0 }, parameters, default, additionalHeaders: headers);
}
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
{
ParameterPositions[method] = position;
}
public override TimeSpan? GetTimeOffset()
{
throw new NotImplementedException();
}
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
=> new TestAuthProvider(credentials);
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
{
throw new NotImplementedException();
}
public override TimeSyncInfo GetTimeSyncInfo()
{
throw new NotImplementedException();
}
}
public class TestRestApi2Client : RestApiClient
{
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
{
RequestFactory = new Mock<IRequestFactory>().Object;
}
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
{
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
}
protected override Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception exception)
{
var errorData = accessor.Deserialize<TestError>();
return new ServerError(errorData.Data.ErrorCode, GetErrorInfo(errorData.Data.ErrorCode, errorData.Data.ErrorMessage));
}
public override TimeSpan? GetTimeOffset()
{
throw new NotImplementedException();
}
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
=> new TestAuthProvider(credentials);
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
{
throw new NotImplementedException();
}
public override TimeSyncInfo GetTimeSyncInfo()
{
throw new NotImplementedException();
}
}
public class TestError
{
[JsonPropertyName("errorCode")]
public int ErrorCode { get; set; }
[JsonPropertyName("errorMessage")]
public string ErrorMessage { get; set; }
}
public class ParseErrorTestRestClient: TestRestClient
{
public ParseErrorTestRestClient() { }
}
}
@@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.MessageParsing;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
using Microsoft.Extensions.Logging;
using Moq;
using CryptoExchange.Net.Testing.Implementations;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Options;
using CryptoExchange.Net.Converters.SystemTextJson;
using System.Net.WebSockets;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
internal class TestSocketClient: BaseSocketClient
{
public TestSubSocketClient SubClient { get; }
/// <summary>
/// Create a new instance of KucoinSocketClient
/// </summary>
/// <param name="optionsFunc">Configure the options to use for this client</param>
public TestSocketClient(Action<TestSocketOptions> optionsDelegate = null)
: this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null)
{
}
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
{
Initialize(options.Value);
SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions));
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
}
public TestSocket CreateSocket()
{
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
}
}
public class TestEnvironment : TradeEnvironment
{
public string TestAddress { get; }
public TestEnvironment(string name, string url) : base(name)
{
TestAddress = url;
}
}
public class TestSocketOptions: SocketExchangeOptions<TestEnvironment>
{
public static TestSocketOptions Default = new TestSocketOptions
{
Environment = new TestEnvironment("Live", "https://test.test")
};
/// <summary>
/// ctor
/// </summary>
public TestSocketOptions()
{
Default?.Set(this);
}
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
internal TestSocketOptions Set(TestSocketOptions targetOptions)
{
targetOptions = base.Set<TestSocketOptions>(targetOptions);
targetOptions.SubOptions = SubOptions.Set(targetOptions.SubOptions);
return targetOptions;
}
}
public class TestSubSocketClient : SocketApiClient
{
private MessagePath _channelPath = MessagePath.Get().Property("channel");
private MessagePath _actionPath = MessagePath.Get().Property("action");
private MessagePath _topicPath = MessagePath.Get().Property("topic");
public Subscription TestSubscription { get; private set; } = null;
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions) : base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
{
}
protected internal override IByteMessageAccessor CreateAccessor(WebSocketMessageType type) => new SystemTextJsonByteMessageAccessor(new System.Text.Json.JsonSerializerOptions());
protected internal override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
internal IWebsocket CreateSocketInternal(string address)
{
return CreateSocket(address);
}
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
=> new TestAuthProvider(credentials);
public CallResult ConnectSocketSub(SocketConnection sub)
{
return ConnectSocketAsync(sub, default).Result;
}
public override string GetListenerIdentifier(IMessageAccessor message)
{
if (!message.IsValid)
{
return "topic";
}
var id = message.GetValue<string>(_channelPath);
id ??= message.GetValue<string>(_topicPath);
return message.GetValue<string>(_actionPath) + "-" + id;
}
public Task<CallResult<UpdateSubscription>> SubscribeToSomethingAsync(string channel, Action<DataEvent<string>> onUpdate, CancellationToken ct)
{
TestSubscription = new TestSubscriptionWithResponseCheck<string>(channel, onUpdate);
return SubscribeAsync(TestSubscription, ct);
}
}
}
@@ -0,0 +1,21 @@
using CryptoExchange.Net.UnitTests.TestImplementations;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(IDictionary<string, string>))]
[JsonSerializable(typeof(Dictionary<string, object>))]
[JsonSerializable(typeof(IDictionary<string, object>))]
[JsonSerializable(typeof(TestObject))]
internal partial class TestSerializerContext : JsonSerializerContext
{
}
}
@@ -1,105 +0,0 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Text;
using CryptoExchange.Net;
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.UnitTests
{
internal class UriSerializationTests
{
[Test]
public void CreateParamString_SerializesBasicValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", "1" },
{ "b", 2 },
{ "c", true }
};
var parameterString = parameters.CreateParamString(false, ArrayParametersSerialization.Array);
Assert.That(parameterString, Is.EqualTo("a=1&b=2&c=True"));
}
[Test]
public void CreateParamString_SerializesArrayValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1", "2" } },
};
var parameterString = parameters.CreateParamString(false, ArrayParametersSerialization.Array);
Assert.That(parameterString, Is.EqualTo("a[]=1&a[]=2"));
}
[Test]
public void CreateParamStringEncoded_SerializesArrayValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1+2", "2+3" } },
};
var parameterString = parameters.CreateParamString(true, ArrayParametersSerialization.Array);
Assert.That(parameterString, Is.EqualTo("a[]=1%2B2&a[]=2%2B3"));
}
[Test]
public void CreateParamString_SerializesJsonArrayValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1", "2" } },
};
var parameterString = parameters.CreateParamString(false, ArrayParametersSerialization.JsonArray);
Assert.That(parameterString, Is.EqualTo("a=[1,2]"));
}
[Test]
public void CreateParamStringEncoded_SerializesJsonArrayValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1+2", "2+3" } },
};
var parameterString = parameters.CreateParamString(true, ArrayParametersSerialization.JsonArray);
Assert.That(parameterString, Is.EqualTo("a=[1%2B2,2%2B3]"));
}
[Test]
public void CreateParamString_SerializesMultipleValuesArrayCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1", "2" } },
};
var parameterString = parameters.CreateParamString(false, ArrayParametersSerialization.MultipleValues);
Assert.That(parameterString, Is.EqualTo("a=1&a=2"));
}
[Test]
public void CreateParamStringEncoded_SerializesMultipleValuesArrayCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1+2", "2+3" } },
};
var parameterString = parameters.CreateParamString(true, ArrayParametersSerialization.MultipleValues);
Assert.That(parameterString, Is.EqualTo("a=1%2B2&a=2%2B3"));
}
}
}
+183
View File
@@ -0,0 +1,183 @@
root = true
[*]
# Indentation and spacing
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
charset = utf-8
max_line_length = 140
insert_final_newline = true
# ReSharper code style properties
resharper_csharp_keep_existing_embedded_arrangement = false
resharper_csharp_place_accessorholder_attribute_on_same_line = false
resharper_csharp_wrap_after_declaration_lpar = true
resharper_csharp_wrap_parameters_style = chop_if_long
resharper_csharp_blank_lines_around_single_line_auto_property = 1
resharper_csharp_keep_blank_lines_in_declarations = 1
resharper_trailing_comma_in_multiline_lists = true
[*.cs]
indent_size = 4
# Code style conventions
dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_object_initializer = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_expression_bodied_methods = true:suggestion
csharp_style_namespace_declarations = file_scoped:warning
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
csharp_prefer_braces = when_multiline:warning
# Analyzer preferences
dotnet_diagnostic.CA2007.severity = warning # Call ConfigureAwait on the awaited Task.
dotnet_code_quality.CA2007.exclude_async_void_methods = true
dotnet_code_quality.CA2007.output_kind = DynamicallyLinkedLibrary
dotnet_diagnostic.CA1000.severity = none # Do not declare static members on generic types
dotnet_diagnostic.CA1051.severity = none # Do not declare visible instance fields
dotnet_diagnostic.CA1510.severity = none # Use ArgumentNullException throw helper
dotnet_diagnostic.CA1720.severity = none # Identifiers should not contain type names
dotnet_diagnostic.CA1716.severity = none # Identifiers should not match keywords
dotnet_diagnostic.CA1835.severity = none # Use ArgumentNullException throw helper
dotnet_diagnostic.CA1846.severity = none # Prefer AsSpan over Substring
dotnet_diagnostic.CA1848.severity = none # Use the LoggerMessage delegates
dotnet_diagnostic.CA1850.severity = none # Prefer static HashData method over ComputeHash
dotnet_diagnostic.CA1866.severity = none # Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char
dotnet_diagnostic.CA2201.severity = none # Do not raise reserved exception types
dotnet_diagnostic.CA2208.severity = none # Do not raise reserved exception types
dotnet_diagnostic.IDE0005.severity = warning # Using directive is unnecessary
[*.xml]
ij_xml_space_inside_empty_tag = true
[*.cs]
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.private_or_internal_field_should_be_fields_start_with__.severity = warning
dotnet_naming_rule.private_or_internal_field_should_be_fields_start_with__.symbols = private_or_internal_field
dotnet_naming_rule.private_or_internal_field_should_be_fields_start_with__.style = fields_start_with__
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
dotnet_naming_symbols.private_or_internal_field.required_modifiers =
# Naming styles
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.fields_start_with__.required_prefix = _
dotnet_naming_style.fields_start_with__.required_suffix =
dotnet_naming_style.fields_start_with__.word_separator =
dotnet_naming_style.fields_start_with__.capitalization = camel_case
csharp_indent_labels = one_less_than_current
csharp_using_directive_placement = outside_namespace:suggestion
csharp_prefer_simple_using_statement = true:suggestion
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_primary_constructors = true:suggestion
csharp_prefer_system_threading_lock = true:suggestion
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:suggestion
csharp_style_expression_bodied_indexers = true:suggestion
csharp_style_expression_bodied_accessors = true:suggestion
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = true:silent
[*.vb]
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
[*.{cs,vb}]
#### Naming styles ####
# Naming rules
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
# Naming styles
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
end_of_line = crlf
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+4 -5
View File
@@ -1,6 +1,5 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit { }
}
namespace System.Runtime.CompilerServices;
internal static class IsExternalInit { }
@@ -1,12 +1,11 @@
using System;
using System;
namespace CryptoExchange.Net.Attributes
namespace CryptoExchange.Net.Attributes;
/// <summary>
/// Used for conversion in ArrayConverter
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class JsonConversionAttribute: Attribute
{
/// <summary>
/// Used for conversion in ArrayConverter
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class JsonConversionAttribute: Attribute
{
}
}
+17 -18
View File
@@ -1,25 +1,24 @@
using System;
using System;
namespace CryptoExchange.Net.Attributes
namespace CryptoExchange.Net.Attributes;
/// <summary>
/// Map a enum entry to string values
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class MapAttribute : Attribute
{
/// <summary>
/// Map a enum entry to string values
/// Values mapping to the enum entry
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class MapAttribute : Attribute
{
/// <summary>
/// Values mapping to the enum entry
/// </summary>
public string[] Values { get; set; }
public string[] Values { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="maps"></param>
public MapAttribute(params string[] maps)
{
Values = maps;
}
/// <summary>
/// ctor
/// </summary>
/// <param name="maps"></param>
public MapAttribute(params string[] maps)
{
Values = maps;
}
}
@@ -1,19 +1,56 @@
namespace CryptoExchange.Net.Authentication
using System;
namespace CryptoExchange.Net.Authentication;
/// <summary>
/// Api credentials, used to sign requests accessing private endpoints
/// </summary>
public class ApiCredentials
{
/// <summary>
/// Api credentials, used to sign requests accessing private endpoints
/// The api key / label to authenticate requests
/// </summary>
public abstract class ApiCredentials
{
/// <summary>
/// Validate the API credentials
/// </summary>
public abstract void Validate();
public string Key { get; set; }
/// <summary>
/// Copy the credentials
/// </summary>
/// <returns></returns>
public abstract ApiCredentials Copy();
/// <summary>
/// The api secret or private key to authenticate requests
/// </summary>
public string Secret { get; set; }
/// <summary>
/// The api passphrase. Not needed on all exchanges
/// </summary>
public string? Pass { get; set; }
/// <summary>
/// Type of the credentials
/// </summary>
public ApiCredentialsType CredentialType { get; set; }
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
/// <param name="key">The api key / label used for identification</param>
/// <param name="secret">The api secret or private key used for signing</param>
/// <param name="pass">The api pass for the key. Not always needed</param>
/// <param name="credentialType">The type of credentials</param>
public ApiCredentials(string key, string secret, string? pass = null, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
throw new ArgumentException("Key and secret can't be null/empty");
CredentialType = credentialType;
Key = key;
Secret = secret;
Pass = pass;
}
/// <summary>
/// Copy the credentials
/// </summary>
/// <returns></returns>
public virtual ApiCredentials Copy()
{
return new ApiCredentials(Key, Secret, Pass, CredentialType);
}
}
@@ -0,0 +1,20 @@
namespace CryptoExchange.Net.Authentication;
/// <summary>
/// Credentials type
/// </summary>
public enum ApiCredentialsType
{
/// <summary>
/// Hmac keys credentials
/// </summary>
Hmac,
/// <summary>
/// Rsa keys credentials in xml format
/// </summary>
RsaXml,
/// <summary>
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
/// </summary>
RsaPem
}
File diff suppressed because it is too large Load Diff
@@ -1,569 +0,0 @@
using System;
using System.Security.Cryptography;
using System.Text;
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
namespace CryptoExchange.Net.Authentication
{
/// <summary>
/// Base class for a set of credentials
/// </summary>
public abstract class CredentialSet : ApiCredentials
{
/// <summary>
/// The (public) key/identifier for this credential pair
/// </summary>
public string Key { get; set; }
/// <summary>
/// ctor
/// </summary>
public CredentialSet() { }
/// <summary>
/// ctor
/// </summary>
public CredentialSet(string key)
{
Key = key;
}
/// <summary>
/// Validate the API credential
/// </summary>
public override void Validate()
{
if (string.IsNullOrEmpty(Key))
throw new ArgumentException($"Key not set on {GetType().Name}", nameof(Key));
}
}
/// <summary>
/// Api key credentials
/// </summary>
public class ApiKeyCredential : CredentialSet
{
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key</param>
public ApiKeyCredential(string key) : base(key)
{
}
/// <inheritdoc />
public override ApiCredentials Copy() => new ApiKeyCredential(Key);
}
/// <summary>
/// HMAC credentials
/// </summary>
public class HMACCredential : CredentialSet
{
private byte[]? _sBytes;
/// <summary>
/// API secret
/// </summary>
public string Secret { get; set; }
/// <summary>
/// ctor
/// </summary>
public HMACCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="secret">Api secret</param>
public HMACCredential(string key, string secret) : base(key)
{
Secret = secret;
}
/// <summary>
/// Get the secret value bytes
/// </summary>
/// <returns></returns>
public byte[] GetSBytes()
{
return _sBytes ??= Encoding.UTF8.GetBytes(Secret);
}
/// <inheritdoc />
public override ApiCredentials Copy() => new HMACCredential(Key, Secret);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(Secret))
throw new ArgumentException($"Secret not set on {GetType().Name}", nameof(Secret));
}
}
/// <summary>
/// HMAC credentials
/// </summary>
public class HMACPassCredential : HMACCredential
{
/// <summary>
/// Passphrase
/// </summary>
public string Pass { get; set; }
/// <summary>
/// ctor
/// </summary>
public HMACPassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="secret">Api secret</param>
/// <param name="pass">Passphrase</param>
public HMACPassCredential(string key, string secret, string pass) : base(key, secret)
{
Pass = pass;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new HMACPassCredential(Key, Secret, Pass);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(Pass))
throw new ArgumentException($"Pass not set on {GetType().Name}", nameof(Pass));
}
}
/// <summary>
/// RSA credentials
/// </summary>
public abstract class RSACredential : CredentialSet
{
/// <summary>
/// Private key
/// </summary>
public string PrivateKey { get; set; }
/// <summary>
/// ctor
/// </summary>
public RSACredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
public RSACredential(string key, string privateKey) : base(key)
{
PrivateKey = privateKey;
}
/// <summary>
/// Get RSA signer
/// </summary>
public abstract RSA GetSigner();
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(PrivateKey))
throw new ArgumentException($"PrivateKey not set on {GetType().Name}", nameof(PrivateKey));
}
}
/// <summary>
/// RSA credentials
/// </summary>
public abstract class RSAPassCredential : RSACredential
{
/// <summary>
/// Passphrase
/// </summary>
public string Pass { get; set; }
/// <summary>
/// ctor
/// </summary>
public RSAPassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
/// <param name="pass">Passphrase</param>
public RSAPassCredential(string key, string privateKey, string pass) : base(key, privateKey)
{
PrivateKey = privateKey;
Pass = pass;
}
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(Pass))
throw new ArgumentException($"PrivateKey not set on {GetType().Name}", nameof(PrivateKey));
}
}
#if NETSTANDARD2_1_OR_GREATER || NET7_0_OR_GREATER
/// <summary>
/// RSA credentials in PEM/base64 format
/// </summary>
public class RSAPemCredential : RSACredential
{
/// <summary>
/// ctor
/// </summary>
public RSAPemCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
public RSAPemCredential(string key, string privateKey) : base(key, privateKey)
{
}
/// <summary>
/// Get RSA signer
/// </summary>
public override RSA GetSigner()
{
var rsa = RSA.Create();
var key = PrivateKey!
.Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Trim();
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(
key)
, out _);
return rsa;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new RSAPemCredential(Key, PrivateKey);
}
/// <summary>
/// RSA PEM/Base64 credentials
/// </summary>
public class RSAPemPassCredential : RSAPassCredential
{
/// <summary>
/// ctor
/// </summary>
public RSAPemPassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="privateKey">Api secret</param>
/// <param name="pass">Passphrase</param>
public RSAPemPassCredential(string key, string privateKey, string pass) : base(key, privateKey, pass)
{
}
/// <summary>
/// Get RSA signer
/// </summary>
public override RSA GetSigner()
{
var rsa = RSA.Create();
var key = PrivateKey!
.Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Trim();
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(
key)
, out _);
return rsa;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new RSAPemPassCredential(Key, PrivateKey, Pass);
}
#endif
/// <summary>
/// RSA credentials in XML format
/// </summary>
public class RSAXmlCredential : RSACredential
{
/// <summary>
/// ctor
/// </summary>
public RSAXmlCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
public RSAXmlCredential(string key, string privateKey) : base(key, privateKey)
{
}
/// <summary>
/// Get RSA signer
/// </summary>
public override RSA GetSigner()
{
var rsa = RSA.Create();
rsa.FromXmlString(PrivateKey);
return rsa;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new RSAXmlCredential(Key, PrivateKey);
}
/// <summary>
/// RSA XML credentials
/// </summary>
public class RSAXmlPassCredential : RSAPassCredential
{
/// <summary>
/// ctor
/// </summary>
public RSAXmlPassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="privateKey">Api secret</param>
/// <param name="pass">Passphrase</param>
public RSAXmlPassCredential(string key, string privateKey, string pass) : base(key, privateKey, pass)
{
}
/// <summary>
/// Get RSA signer
/// </summary>
public override RSA GetSigner()
{
var rsa = RSA.Create();
rsa.FromXmlString(PrivateKey);
return rsa;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new RSAXmlPassCredential(Key, PrivateKey, Pass);
}
#if NET8_0_OR_GREATER
/// <summary>
/// Credentials in Ed25519 format
/// </summary>
public class Ed25519Credential : CredentialSet
{
private NSec.Cryptography.Key? _signKey;
/// <summary>
/// Private key
/// </summary>
public string PrivateKey { get; set; }
/// <summary>
/// ctor
/// </summary>
public Ed25519Credential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
public Ed25519Credential(string key, string privateKey) : base(key)
{
PrivateKey = privateKey;
}
/// <summary>
/// Get signing key
/// </summary>
public NSec.Cryptography.Key GetSigningKey()
{
if (_signKey != null)
return _signKey;
var key = PrivateKey!
.Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Trim();
var keyBytes = Convert.FromBase64String(key);
_signKey = NSec.Cryptography.Key.Import(NSec.Cryptography.SignatureAlgorithm.Ed25519, keyBytes, NSec.Cryptography.KeyBlobFormat.PkixPrivateKey);
return _signKey;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new Ed25519Credential(Key, PrivateKey);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(PrivateKey))
throw new ArgumentException($"PrivateKey not set on {GetType().Name}", nameof(PrivateKey));
}
}
/// <summary>
/// Ed25519 credentials
/// </summary>
public class Ed25519PassCredential : Ed25519Credential
{
/// <summary>
/// Passphrase
/// </summary>
public string Pass { get; set; }
/// <summary>
/// ctor
/// </summary>
public Ed25519PassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="privateKey">Private key</param>
/// <param name="pass">Passphrase</param>
public Ed25519PassCredential(string key, string privateKey, string pass) : base(key, privateKey)
{
Pass = pass;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new Ed25519PassCredential(Key, PrivateKey, Pass);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(Pass))
throw new ArgumentException($"Pass not set on {GetType().Name}", nameof(Pass));
}
}
#endif
/// <summary>
/// Credentials in ECDsa format
/// </summary>
public class ECDsaCredential : CredentialSet
{
/// <summary>
/// Private key
/// </summary>
public string PrivateKey { get; set; }
/// <summary>
/// ctor
/// </summary>
public ECDsaCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Public key</param>
/// <param name="privateKey">Private key</param>
public ECDsaCredential(string key, string privateKey) : base(key)
{
PrivateKey = privateKey;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new ECDsaCredential(Key, PrivateKey);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(PrivateKey))
throw new ArgumentException($"PrivateKey not set on {GetType().Name}", nameof(PrivateKey));
}
}
/// <summary>
/// ECDsa credentials
/// </summary>
public class ECDsaPassCredential : ECDsaCredential
{
/// <summary>
/// Passphrase
/// </summary>
public string Pass { get; set; }
/// <summary>
/// ctor
/// </summary>
public ECDsaPassCredential()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="key">Api key/label</param>
/// <param name="privateKey">Private key</param>
/// <param name="pass">Passphrase</param>
public ECDsaPassCredential(string key, string privateKey, string pass) : base(key, privateKey)
{
Pass = pass;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new ECDsaPassCredential(Key, PrivateKey, Pass);
/// <inheritdoc />
public override void Validate()
{
base.Validate();
if (string.IsNullOrEmpty(Pass))
throw new ArgumentException($"Pass not set on {GetType().Name}", nameof(Pass));
}
}
}
@@ -1,17 +1,16 @@
namespace CryptoExchange.Net.Authentication
namespace CryptoExchange.Net.Authentication;
/// <summary>
/// Output string type
/// </summary>
public enum SignOutputType
{
/// <summary>
/// Output string type
/// Hex string
/// </summary>
public enum SignOutputType
{
/// <summary>
/// Hex string
/// </summary>
Hex,
/// <summary>
/// Base64 string
/// </summary>
Base64
}
Hex,
/// <summary>
/// Base64 string
/// </summary>
Base64
}
@@ -1,147 +0,0 @@
using System;
using System.Linq;
using System.Numerics;
using System.Text;
namespace CryptoExchange.Net.Authentication.Signing
{
/// <summary>
/// ABI encoding
/// </summary>
public static class CeAbiEncoder
{
/// <summary>
/// ABI encode string as Sha3Keccack hashed byte value
/// </summary>
public static byte[] AbiValueEncodeString(string value)
{
var abiValueEncoded = CeSha3Keccack.CalculateHash(Encoding.UTF8.GetBytes(value));
return abiValueEncoded;
}
/// <summary>
/// ABI encode bool value as uint256 with 1 for true and 0 for false, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeBool(bool value)
=> AbiValueEncodeInt((byte)(value ? 1 : 0));
/// <summary>
/// ABI encode byte value as uint256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeInt(byte value)
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
/// <summary>
/// ABI encode short value as int256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeInt(short value)
=> AbiValueEncodeBigInteger(true, new BigInteger(value));
/// <summary>
/// ABI encode int value as int256, as per ABI specification
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static byte[] AbiValueEncodeInt(int value)
=> AbiValueEncodeBigInteger(true, new BigInteger(value));
/// <summary>
/// ABI encode long value as int256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeInt(long value)
=> AbiValueEncodeBigInteger(true, new BigInteger(value));
/// <summary>
/// ABI encode ushort value as uint256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeInt(ushort value)
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
/// <summary>
/// ABI encode uint value as uint256, as per ABI specification
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static byte[] AbiValueEncodeInt(uint value)
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
/// <summary>
/// ABI encode ulong value as uint256, as per ABI specification
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static byte[] AbiValueEncodeInt(ulong value)
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
/// <summary>
/// ABI encode big integer value as int256 or uint256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeBigInteger(bool signed, BigInteger value)
{
var result = new byte[32];
if (signed && value < 0)
{
// Pad with FF
for (int i = 0; i < result.Length; i++)
{
result[i] = 0xFF;
}
}
var t = value.ToByteArray();
if (t.Length == 33)
{
// Strip last byte
var strip1 = new byte[32];
Array.Copy(t, 0, strip1, 0, 32);
t = strip1;
}
if (BitConverter.IsLittleEndian)
t = t.AsEnumerable().Reverse().ToArray();
t.CopyTo(result, result.Length - t.Length);
return result;
}
/// <summary>
/// ABI encode address value as uint256, as per ABI specification
/// </summary>
public static byte[] AbiValueEncodeAddress(string value)
{
var result = new byte[32];
var h = value.HexStringToBytes();
h.CopyTo(result, result.Length - h.Length);
return result;
}
/// <summary>
/// ABI encode hex string value as bytes32, as per ABI specification. The hex string is expected to be a 0x prefixed string, and the resulting bytes will be right aligned in the 32 bytes result, with leading zeros if the hex string is shorter than 32 bytes. If the hex string is longer than 32 bytes, an exception will be thrown.
/// </summary>
/// <param name="length"></param>
/// <param name="value"></param>
/// <returns></returns>
public static byte[] AbiValueEncodeHexBytes(int length, string value)
=> AbiValueEncodeBytes(value.Length, value.HexStringToBytes());
/// <summary>
/// ABI encode byte array value as bytes32, as per ABI specification. The resulting bytes will be right aligned in the 32 bytes result, with leading zeros if the byte array is shorter than 32 bytes. If the byte array is longer than 32 bytes, an exception will be thrown.
/// </summary>
/// <param name="length"></param>
/// <param name="value"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static byte[] AbiValueEncodeBytes(int length, byte[] value)
{
if (length != 32)
throw new Exception("Only 32 bytes size supported");
if (value.Length == 32)
return value;
var result = new byte[32];
value.CopyTo(result, result.Length - value.Length);
return result;
}
}
}
@@ -1,304 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Text.RegularExpressions;
namespace CryptoExchange.Net.Authentication.Signing
{
/// <summary>
/// EIP712 Typed Data Encoder
/// </summary>
public static class CeEip712TypedDataEncoder
{
/// <summary>
/// Encode EIP712 typed data according to the specification, with the provided primary type, domain fields and message fields.
/// The resulting byte array is the 0x19 0x01 prefix followed by the hash of the domain and the hash of the message, which can be signed with ECDsa secp256k1 to produce a signature that can be verified on chain with EIP712.
/// Note that this implementation does not support all possible EIP712 types, but it should cover most common use cases
/// </summary>
public static byte[] EncodeEip721(
string primaryType,
IEnumerable<(string Name, string Type, object Value)> domainFields,
IEnumerable<(string Name, string Type, object Value)> messageFields)
{
var data = new CeTypedDataRaw()
{
PrimaryType = primaryType,
DomainRawValues = domainFields.Select(x => new CeMemberValue
{
TypeName = x.Type,
Value = x.Value,
}).ToArray(),
Message = messageFields.Select(x => new CeMemberValue
{
TypeName = x.Type,
Value = x.Value,
}).ToArray(),
Types = new Dictionary<string, CeMemberDescription[]>
{
{
"EIP712Domain",
domainFields.Select(x => new CeMemberDescription
{
Name = x.Name,
Type = x.Type
}).ToArray()
},
{
primaryType,
messageFields.Select(x => new CeMemberDescription
{
Name = x.Name,
Type = x.Type
}).ToArray()
}
}
};
return EncodeTypedDataRaw(data);
}
/// <summary>
/// Encode EIP712 typed data according to the specification, with the provided primary type, domain fields and message fields.
/// The resulting byte array is the 0x19 0x01 prefix followed by the hash of the domain and the hash of the message, which can be signed with ECDsa secp256k1 to produce a signature that can be verified on chain with EIP712.
/// Note that this implementation does not support all possible EIP712 types, but it should cover most common use cases
/// </summary>
public static byte[] EncodeTypedDataRaw(CeTypedDataRaw typedData)
{
using var memoryStream = new MemoryStream();
using var writer = new BinaryWriter(memoryStream);
// Write 0x19 0x01 prefix
writer.Write((byte)0x19);
writer.Write((byte)0x01);
// Write domain
writer.Write(HashStruct(typedData.Types, "EIP712Domain", typedData.DomainRawValues));
// Write message
writer.Write(HashStruct(typedData.Types, typedData.PrimaryType, typedData.Message));
writer.Flush();
var result = memoryStream.ToArray();
return result;
}
private static byte[] HashStruct(IDictionary<string, CeMemberDescription[]> types, string primaryType, IEnumerable<CeMemberValue> message)
{
var memoryStream = new MemoryStream();
var writer = new BinaryWriter(memoryStream);
// Encode the type header
EncodeType(writer, types, primaryType);
// Encode the data
EncodeData(writer, types, message);
writer.Flush();
return CeSha3Keccack.CalculateHash(memoryStream.ToArray());
}
private static void EncodeData(BinaryWriter writer, IDictionary<string, CeMemberDescription[]> types, IEnumerable<CeMemberValue> memberValues)
{
foreach (var memberValue in memberValues)
{
switch (memberValue.TypeName)
{
case var refType when IsReferenceType(refType):
writer.Write(HashStruct(types, memberValue.TypeName, (IEnumerable<CeMemberValue>)memberValue.Value));
break;
case "string":
writer.Write(CeAbiEncoder.AbiValueEncodeString((string)memberValue.Value));
break;
case "bool":
writer.Write(CeAbiEncoder.AbiValueEncodeBool((bool)memberValue.Value));
break;
case "address":
writer.Write(CeAbiEncoder.AbiValueEncodeAddress((string)memberValue.Value));
break;
default:
if (memberValue.TypeName.Contains("["))
{
var items = (IList)memberValue.Value;
var itemsMemberValues = new List<CeMemberValue>();
foreach (var item in items)
{
itemsMemberValues.Add(new CeMemberValue()
{
TypeName = memberValue.TypeName.Substring(0, memberValue.TypeName.LastIndexOf("[")),
Value = item
});
}
var memoryStream = new MemoryStream();
var writerItem = new BinaryWriter(memoryStream);
EncodeData(writerItem, types, itemsMemberValues);
writerItem.Flush();
writer.Write(CeSha3Keccack.CalculateHash(memoryStream.ToArray()));
}
else if (memberValue.TypeName.StartsWith("int") || memberValue.TypeName.StartsWith("uint"))
{
if (memberValue.Value is string v)
{
if (!BigInteger.TryParse(v, out BigInteger parsedOutput))
throw new Exception($"Failed to encode BigInteger string {v}");
writer.Write(CeAbiEncoder.AbiValueEncodeBigInteger(memberValue.TypeName[0] != 'u', parsedOutput));
}
else if (memberValue.Value is byte b)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(b));
}
else if (memberValue.Value is short s)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(s));
}
else if (memberValue.Value is int i)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(i));
}
else if (memberValue.Value is long l)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(l));
}
else if (memberValue.Value is ushort us)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(us));
}
else if (memberValue.Value is uint ui)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(ui));
}
else if (memberValue.Value is ulong ul)
{
writer.Write(CeAbiEncoder.AbiValueEncodeInt(ul));
}
else
{
throw new Exception("Unknown number value");
}
}
else if (memberValue.TypeName.StartsWith("bytes"))
{
var length = memberValue.TypeName.Length == 5 ? 32 : int.Parse(memberValue.TypeName.Substring(5));
writer.Write(CeAbiEncoder.AbiValueEncodeBytes(length, (byte[])memberValue.Value));
}
break;
}
}
}
private static void EncodeType(BinaryWriter writer, IDictionary<string, CeMemberDescription[]> types, string typeName)
{
var encodedTypes = EncodeTypes(types, typeName);
var encodedPrimaryType = encodedTypes.Single(x => x.Key == typeName);
var encodedReferenceTypes = encodedTypes.Where(x => x.Key != typeName).OrderBy(x => x.Key).Select(x => x.Value);
var fullyEncodedType = encodedPrimaryType.Value + string.Join(string.Empty, encodedReferenceTypes.ToArray());
writer.Write(CeSha3Keccack.CalculateHash(Encoding.UTF8.GetBytes(fullyEncodedType)));
}
/// <summary>
/// Create a list of type => type(parameters), for example:<br />
/// { IP712Domain, EIP712Domain(string name,string version,uint256 chainId,address verifyingContract) }
/// </summary>
private static IList<KeyValuePair<string, string>> EncodeTypes(IDictionary<string, CeMemberDescription[]> types, string currentTypeName)
{
var currentTypeMembers = types[currentTypeName];
var currentTypeMembersEncoded = currentTypeMembers.Select(x => x.Type + " " + x.Name);
var result = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(currentTypeName, currentTypeName + "(" + string.Join(",", currentTypeMembersEncoded.ToArray()) + ")")
};
result.AddRange(currentTypeMembers.Select(x => x.Type.Contains("[") ? x.Type.Substring(0, x.Type.IndexOf("[")) : x.Type)
.Distinct()
.Where(IsReferenceType)
.SelectMany(x => EncodeTypes(types, x)));
return result;
}
internal static bool IsReferenceType(string typeName)
{
switch (typeName)
{
case var bytes when new Regex("bytes\\d+").IsMatch(bytes):
case var @uint when new Regex("uint\\d+").IsMatch(@uint):
case var @int when new Regex("int\\d+").IsMatch(@int):
case "bytes":
case "string":
case "bool":
case "address":
case var array when array.Contains("["):
return false;
default:
return true;
}
}
}
/// <summary>
/// Member description
/// </summary>
public class CeMemberDescription
{
/// <summary>
/// Name
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Type
/// </summary>
public string Type { get; set; } = string.Empty;
}
/// <summary>
/// Member value
/// </summary>
public class CeMemberValue
{
/// <summary>
/// Type name
/// </summary>
public string TypeName { get; set; } = string.Empty;
/// <summary>
/// Value
/// </summary>
public object Value { get; set; } = string.Empty;
}
/// <summary>
/// Typed data raw, used for encoding EIP712 typed data with the provided primary type, domain fields and message fields.
/// </summary>
public class CeTypedDataRaw
{
/// <summary>
/// Type dictionary
/// </summary>
public IDictionary<string, CeMemberDescription[]> Types { get; set; } = new Dictionary<string, CeMemberDescription[]>();
/// <summary>
/// Primary type
/// </summary>
public string PrimaryType { get; set; } = string.Empty;
/// <summary>
/// Message values
/// </summary>
public CeMemberValue[] Message { get; set; } = [];
/// <summary>
/// Domain values
/// </summary>
public CeMemberValue[] DomainRawValues { get; set; } = [];
}
}
@@ -1,385 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
namespace CryptoExchange.Net.Authentication.Signing
{
/// <summary>
/// Sha3 Keccack hashing, as per Ethereum specification, with 256 bit output
/// </summary>
public class CeSha3Keccack
{
/// <summary>
/// Calculate the Keccack256 hash of the provided data, as per Ethereum specification
/// </summary>
public static byte[] CalculateHash(byte[] data)
{
var digest = new CeKeccakDigest256();
var output = new byte[digest.GetDigestSize()];
digest.BlockUpdate(data, data.Length);
digest.DoFinal(output, 0);
return output;
}
}
internal class CeKeccakDigest256
{
private static readonly ulong[] _keccakRoundConstants = KeccakInitializeRoundConstants();
private static readonly int[] _keccakRhoOffsets = KeccakInitializeRhoOffsets();
private readonly int _rate;
private const int _stateLength = 1600 / 8;
private readonly ulong[] _state = new ulong[_stateLength / 8];
private readonly byte[] _dataQueue = new byte[1536 / 8];
private int _bitsInQueue;
private int _fixedOutputLength;
private bool _squeezing;
private int _bitsAvailableForSqueezing;
public CeKeccakDigest256()
{
_rate = 1600 - (256 << 1);
_bitsInQueue = 0;
_squeezing = false;
_bitsAvailableForSqueezing = 0;
_fixedOutputLength = 1600 - _rate >> 1;
}
internal void BlockUpdate(byte[] data, int length)
{
int bytesInQueue = _bitsInQueue >> 3;
int rateBytes = _rate >> 3;
int count = 0;
while (count < length)
{
if (bytesInQueue == 0 && count <= length - rateBytes)
{
do
{
KeccakAbsorb(data, count);
count += rateBytes;
} while (count <= length - rateBytes);
}
else
{
int partialBlock = Math.Min(rateBytes - bytesInQueue, length - count);
Array.Copy(data, count, _dataQueue, bytesInQueue, partialBlock);
bytesInQueue += partialBlock;
count += partialBlock;
if (bytesInQueue == rateBytes)
{
KeccakAbsorb(_dataQueue, 0);
bytesInQueue = 0;
}
}
}
_bitsInQueue = bytesInQueue << 3;
}
internal void DoFinal(byte[] output, int outOff)
{
Squeeze(output, outOff, _fixedOutputLength >> 3);
}
internal int GetDigestSize() => _fixedOutputLength >> 3;
protected void Squeeze(byte[] output, int off, int len)
{
if (!_squeezing)
PadAndSwitchToSqueezingPhase();
long outputLength = (long)len << 3;
long i = 0;
while (i < outputLength)
{
if (_bitsAvailableForSqueezing == 0)
{
KeccakPermutation();
KeccakExtract();
_bitsAvailableForSqueezing = _rate;
}
int partialBlock = (int)Math.Min(_bitsAvailableForSqueezing, outputLength - i);
Array.Copy(_dataQueue, _rate - _bitsAvailableForSqueezing >> 3, output, off + (int)(i >> 3),
partialBlock >> 3);
_bitsAvailableForSqueezing -= partialBlock;
i += partialBlock;
}
}
private static ulong[] KeccakInitializeRoundConstants()
{
ulong[] keccakRoundConstants = new ulong[24];
byte LFSRState = 0x01;
for (int i = 0; i < 24; i++)
{
keccakRoundConstants[i] = 0;
for (int j = 0; j < 7; j++)
{
int bitPosition = (1 << j) - 1;
// LFSR86540
bool loBit = (LFSRState & 0x01) != 0;
if (loBit)
keccakRoundConstants[i] ^= 1UL << bitPosition;
bool hiBit = (LFSRState & 0x80) != 0;
LFSRState <<= 1;
if (hiBit)
LFSRState ^= 0x71;
}
}
return keccakRoundConstants;
}
private static int[] KeccakInitializeRhoOffsets()
{
int[] keccakRhoOffsets = new int[25];
int x, y, t, newX, newY;
int rhoOffset = 0;
keccakRhoOffsets[0] = rhoOffset;
x = 1;
y = 0;
for (t = 1; t < 25; t++)
{
rhoOffset = rhoOffset + t & 63;
keccakRhoOffsets[x % 5 + 5 * (y % 5)] = rhoOffset;
newX = (0 * x + 1 * y) % 5;
newY = (2 * x + 3 * y) % 5;
x = newX;
y = newY;
}
return keccakRhoOffsets;
}
private void KeccakAbsorb(byte[] data, int off)
{
int count = _rate >> 6;
for (int i = 0; i < count; ++i)
{
_state[i] ^= Pack.LeToUInt64(data, off);
off += 8;
}
KeccakPermutation();
}
private void KeccakPermutation()
{
for (int i = 0; i < 24; i++)
{
Theta(_state);
Rho(_state);
Pi(_state);
Chi(_state);
Iota(_state, i);
}
}
private static ulong LeftRotate(ulong v, int r)
{
return v << r | v >> -r;
}
private static void Theta(ulong[] A)
{
ulong C0 = A[0 + 0] ^ A[0 + 5] ^ A[0 + 10] ^ A[0 + 15] ^ A[0 + 20];
ulong C1 = A[1 + 0] ^ A[1 + 5] ^ A[1 + 10] ^ A[1 + 15] ^ A[1 + 20];
ulong C2 = A[2 + 0] ^ A[2 + 5] ^ A[2 + 10] ^ A[2 + 15] ^ A[2 + 20];
ulong C3 = A[3 + 0] ^ A[3 + 5] ^ A[3 + 10] ^ A[3 + 15] ^ A[3 + 20];
ulong C4 = A[4 + 0] ^ A[4 + 5] ^ A[4 + 10] ^ A[4 + 15] ^ A[4 + 20];
ulong dX = LeftRotate(C1, 1) ^ C4;
A[0] ^= dX;
A[5] ^= dX;
A[10] ^= dX;
A[15] ^= dX;
A[20] ^= dX;
dX = LeftRotate(C2, 1) ^ C0;
A[1] ^= dX;
A[6] ^= dX;
A[11] ^= dX;
A[16] ^= dX;
A[21] ^= dX;
dX = LeftRotate(C3, 1) ^ C1;
A[2] ^= dX;
A[7] ^= dX;
A[12] ^= dX;
A[17] ^= dX;
A[22] ^= dX;
dX = LeftRotate(C4, 1) ^ C2;
A[3] ^= dX;
A[8] ^= dX;
A[13] ^= dX;
A[18] ^= dX;
A[23] ^= dX;
dX = LeftRotate(C0, 1) ^ C3;
A[4] ^= dX;
A[9] ^= dX;
A[14] ^= dX;
A[19] ^= dX;
A[24] ^= dX;
}
private static void Rho(ulong[] A)
{
// KeccakRhoOffsets[0] == 0
for (int x = 1; x < 25; x++)
{
A[x] = LeftRotate(A[x], _keccakRhoOffsets[x]);
}
}
private static void Pi(ulong[] A)
{
ulong a1 = A[1];
A[1] = A[6];
A[6] = A[9];
A[9] = A[22];
A[22] = A[14];
A[14] = A[20];
A[20] = A[2];
A[2] = A[12];
A[12] = A[13];
A[13] = A[19];
A[19] = A[23];
A[23] = A[15];
A[15] = A[4];
A[4] = A[24];
A[24] = A[21];
A[21] = A[8];
A[8] = A[16];
A[16] = A[5];
A[5] = A[3];
A[3] = A[18];
A[18] = A[17];
A[17] = A[11];
A[11] = A[7];
A[7] = A[10];
A[10] = a1;
}
private static void Chi(ulong[] A)
{
ulong chiC0, chiC1, chiC2, chiC3, chiC4;
for (int yBy5 = 0; yBy5 < 25; yBy5 += 5)
{
chiC0 = A[0 + yBy5] ^ ~A[(0 + 1) % 5 + yBy5] & A[(0 + 2) % 5 + yBy5];
chiC1 = A[1 + yBy5] ^ ~A[(1 + 1) % 5 + yBy5] & A[(1 + 2) % 5 + yBy5];
chiC2 = A[2 + yBy5] ^ ~A[(2 + 1) % 5 + yBy5] & A[(2 + 2) % 5 + yBy5];
chiC3 = A[3 + yBy5] ^ ~A[(3 + 1) % 5 + yBy5] & A[(3 + 2) % 5 + yBy5];
chiC4 = A[4 + yBy5] ^ ~A[(4 + 1) % 5 + yBy5] & A[(4 + 2) % 5 + yBy5];
A[0 + yBy5] = chiC0;
A[1 + yBy5] = chiC1;
A[2 + yBy5] = chiC2;
A[3 + yBy5] = chiC3;
A[4 + yBy5] = chiC4;
}
}
private static void Iota(ulong[] A, int indexRound)
{
A[0] ^= _keccakRoundConstants[indexRound];
}
private void PadAndSwitchToSqueezingPhase()
{
Debug.Assert(_bitsInQueue < _rate);
_dataQueue[_bitsInQueue >> 3] |= (byte)(1U << (_bitsInQueue & 7));
if (++_bitsInQueue == _rate)
{
KeccakAbsorb(_dataQueue, 0);
_bitsInQueue = 0;
}
{
int full = _bitsInQueue >> 6, partial = _bitsInQueue & 63;
int off = 0;
for (int i = 0; i < full; ++i)
{
_state[i] ^= Pack.LeToUInt64(_dataQueue, off);
off += 8;
}
if (partial > 0)
{
ulong mask = (1UL << partial) - 1UL;
_state[full] ^= Pack.LeToUInt64(_dataQueue, off) & mask;
}
_state[_rate - 1 >> 6] ^= 1UL << 63;
}
KeccakPermutation();
KeccakExtract();
_bitsAvailableForSqueezing = _rate;
_bitsInQueue = 0;
_squeezing = true;
}
private void KeccakExtract()
{
Pack.UInt64ToLe(_state, 0, _rate >> 6, _dataQueue, 0);
}
static class Pack
{
internal static ulong LeToUInt64(byte[] bs, int off)
{
uint lo = LeToUInt32(bs, off);
uint hi = LeToUInt32(bs, off + 4);
return (ulong)hi << 32 | lo;
}
internal static uint LeToUInt32(byte[] bs, int off)
{
return bs[off]
| (uint)bs[off + 1] << 8
| (uint)bs[off + 2] << 16
| (uint)bs[off + 3] << 24;
}
internal static void UInt64ToLe(ulong[] ns, int nsOff, int nsLen, byte[] bs, int bsOff)
{
for (int i = 0; i < nsLen; ++i)
{
UInt64ToLe(ns[nsOff + i], bs, bsOff);
bsOff += 8;
}
}
internal static void UInt64ToLe(ulong n, byte[] bs, int off)
{
UInt32ToLe((uint)n, bs, off);
UInt32ToLe((uint)(n >> 32), bs, off + 4);
}
internal static void UInt32ToLe(uint n, byte[] bs, int off)
{
bs[off] = (byte)n;
bs[off + 1] = (byte)(n >> 8);
bs[off + 2] = (byte)(n >> 16);
bs[off + 3] = (byte)(n >> 24);
}
}
}
}
+42 -48
View File
@@ -1,58 +1,52 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
namespace CryptoExchange.Net.Caching
namespace CryptoExchange.Net.Caching;
internal class MemoryCache
{
internal class MemoryCache
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
private readonly object _lock = new object();
/// <summary>
/// Add a new cache entry. Will override an existing entry if it already exists
/// </summary>
/// <param name="key">The key identifier</param>
/// <param name="value">Cache value</param>
public void Add(string key, object value)
{
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
#if NET9_0_OR_GREATER
private readonly Lock _lock = new Lock();
#else
private readonly object _lock = new object();
#endif
var cacheItem = new CacheItem(DateTime.UtcNow, value);
_cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem);
}
/// <summary>
/// Add a new cache entry. Will override an existing entry if it already exists
/// </summary>
/// <param name="key">The key identifier</param>
/// <param name="value">Cache value</param>
public void Add(string key, object value)
/// <summary>
/// Get a cached value
/// </summary>
/// <param name="key">The key identifier</param>
/// <param name="maxAge">The max age of the cached entry</param>
/// <returns>Cached value if it was in cache</returns>
public object? Get(string key, TimeSpan maxAge)
{
foreach (var item in _cache.Where(x => DateTime.UtcNow - x.Value.CacheTime > maxAge).ToList())
_cache.TryRemove(item.Key, out _);
_cache.TryGetValue(key, out CacheItem? value);
if (value == null)
return null;
return value.Value;
}
private class CacheItem
{
public DateTime CacheTime { get; }
public object Value { get; }
public CacheItem(DateTime cacheTime, object value)
{
var cacheItem = new CacheItem(DateTime.UtcNow, value);
_cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem);
}
/// <summary>
/// Get a cached value
/// </summary>
/// <param name="key">The key identifier</param>
/// <param name="maxAge">The max age of the cached entry</param>
/// <returns>Cached value if it was in cache</returns>
public object? Get(string key, TimeSpan maxAge)
{
foreach (var item in _cache.Where(x => DateTime.UtcNow - x.Value.CacheTime > maxAge).ToList())
_cache.TryRemove(item.Key, out _);
_cache.TryGetValue(key, out CacheItem? value);
if (value == null)
return null;
return value.Value;
}
private class CacheItem
{
public DateTime CacheTime { get; }
public object Value { get; }
public CacheItem(DateTime cacheTime, object value)
{
CacheTime = cacheTime;
Value = value;
}
CacheTime = cacheTime;
Value = value;
}
}
}
+117 -98
View File
@@ -1,121 +1,140 @@
using System;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Clients
namespace CryptoExchange.Net.Clients;
/// <summary>
/// Base API for all API clients
/// </summary>
public abstract class BaseApiClient : IDisposable, IBaseApiClient
{
/// <summary>
/// Base API for all API clients
/// Logger
/// </summary>
public abstract class BaseApiClient : IDisposable, IBaseApiClient
protected ILogger _logger;
/// <summary>
/// If we are disposing
/// </summary>
protected bool _disposing;
/// <summary>
/// The authentication provider for this API client. (null if no credentials are set)
/// </summary>
public AuthenticationProvider? AuthenticationProvider { get; private set; }
/// <summary>
/// The environment this client communicates to
/// </summary>
public string BaseAddress { get; }
/// <summary>
/// Output the original string data along with the deserialized object
/// </summary>
public bool OutputOriginalData { get; }
/// <inheritdoc />
public bool Authenticated => ApiCredentials != null;
/// <inheritdoc />
public ApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// Api options
/// </summary>
public ApiOptions ApiOptions { get; }
/// <summary>
/// Client Options
/// </summary>
public ExchangeOptions ClientOptions { get; }
/// <summary>
/// Mapping of a response code to known error types
/// </summary>
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</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="apiCredentials">Api credentials</param>
/// <param name="clientOptions">Client options</param>
/// <param name="apiOptions">Api options</param>
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions)
{
/// <summary>
/// Client name
/// </summary>
protected string? _clientName;
_logger = logger;
/// <summary>
/// Logger
/// </summary>
protected ILogger _logger;
ClientOptions = clientOptions;
ApiOptions = apiOptions;
OutputOriginalData = outputOriginalData;
BaseAddress = baseAddress;
ApiCredentials = apiCredentials?.Copy();
/// <summary>
/// If we are disposing
/// </summary>
protected bool _disposing;
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <summary>
/// Whether a proxy is configured
/// </summary>
protected bool _proxyConfigured;
/// <summary>
/// Create an AuthenticationProvider implementation instance based on the provided credentials
/// </summary>
/// <param name="credentials"></param>
/// <returns></returns>
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
/// <summary>
/// Name of the client
/// </summary>
protected internal string ClientName
{
get
{
if (_clientName != null)
return _clientName;
/// <inheritdoc />
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
_clientName = GetType().Name;
return _clientName;
}
}
/// <summary>
/// Get error info for a response code
/// </summary>
public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message);
/// <summary>
/// The environment this client communicates to
/// </summary>
public string BaseAddress { get; }
/// <summary>
/// Get error info for a response code
/// </summary>
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
/// <summary>
/// Output the original string data along with the deserialized object
/// </summary>
public bool OutputOriginalData { get; }
/// <inheritdoc />
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
ApiCredentials = credentials?.Copy();
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <summary>
/// Api options
/// </summary>
public ApiOptions ApiOptions { get; }
/// <inheritdoc />
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
{
ClientOptions.Proxy = options.Proxy;
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
/// <summary>
/// Client Options
/// </summary>
public ExchangeOptions ClientOptions { get; }
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
if (ApiCredentials != null)
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
}
/// <summary>
/// Mapping of a response code to known error types
/// </summary>
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</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,
bool outputOriginalData,
string baseAddress,
ExchangeOptions clientOptions,
ApiOptions apiOptions)
{
_logger = logger;
ClientOptions = clientOptions;
ApiOptions = apiOptions;
OutputOriginalData = outputOriginalData;
BaseAddress = baseAddress;
_proxyConfigured = ClientOptions.Proxy != null;
}
/// <inheritdoc />
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
/// <summary>
/// Get error info for a response code
/// </summary>
public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message);
/// <summary>
/// Get error info for a response code
/// </summary>
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose()
{
_disposing = true;
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose(bool disposing)
{
_disposing = true;
}
}
+115 -102
View File
@@ -1,129 +1,142 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Threading;
namespace CryptoExchange.Net.Clients
namespace CryptoExchange.Net.Clients;
/// <summary>
/// The base for all clients, websocket client and rest client
/// </summary>
public abstract class BaseClient : IDisposable
{
/// <summary>
/// The base for all clients, websocket client and rest client
/// Version of the CryptoExchange.Net base library
/// </summary>
public abstract class BaseClient : IDisposable
{
/// <summary>
/// Version of the CryptoExchange.Net base library
/// </summary>
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
/// <summary>
/// Version of the client implementation
/// </summary>
public Version ExchangeLibVersion
{
get
/// <summary>
/// Version of the client implementation
/// </summary>
public Version ExchangeLibVersion
{
get
{
lock(_versionLock)
{
lock(_versionLock)
{
if (_exchangeVersion == null)
_exchangeVersion = GetType().Assembly.GetName().Version!;
if (_exchangeVersion == null)
_exchangeVersion = GetType().Assembly.GetName().Version!;
return _exchangeVersion;
}
return _exchangeVersion;
}
}
}
/// <summary>
/// The name of the API the client is for
/// </summary>
public string Exchange { get; }
/// <summary>
/// The name of the API the client is for
/// </summary>
public string Exchange { get; }
/// <summary>
/// Whether client is disposed
/// </summary>
public bool Disposed { get; private set; }
/// <summary>
/// Api clients in this client
/// </summary>
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
/// <summary>
/// Api clients in this client
/// </summary>
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
/// <summary>
/// The log object
/// </summary>
protected internal ILogger _logger;
/// <summary>
/// The log object
/// </summary>
protected internal ILogger _logger;
private readonly object _versionLock = new object();
private Version _exchangeVersion;
#if NET9_0_OR_GREATER
private readonly Lock _versionLock = new Lock();
#else
private readonly object _versionLock = new object();
#endif
private Version _exchangeVersion;
/// <summary>
/// Provided client options
/// </summary>
public ExchangeOptions ClientOptions { get; private set; }
/// <summary>
/// Provided client options
/// </summary>
public ExchangeOptions ClientOptions { get; private set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="exchange">The name of the exchange this client is for</param>
/// <summary>
/// ctor
/// </summary>
/// <param name="logger">Logger</param>
/// <param name="exchange">The name of the exchange this client is for</param>
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
protected BaseClient(ILoggerFactory? logger, string exchange)
protected BaseClient(ILoggerFactory? logger, string exchange)
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
{
Exchange = exchange;
}
/// <summary>
/// Initialize the client with the specified options
/// </summary>
/// <param name="options"></param>
/// <exception cref="ArgumentNullException"></exception>
protected virtual void Initialize(ExchangeOptions options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
ClientOptions = options;
_logger.Log(LogLevel.Trace, "Client configuration: {Options}, CryptoExchange.Net: v{CryptoExchangeVersion}, {Exchange}.Net: v{ExchangeVersion}", options, CryptoExchangeLibVersion, Exchange, ExchangeLibVersion);
}
/// <summary>
/// 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>
protected virtual void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
}
/// <summary>
/// Register an API client
/// </summary>
/// <param name="apiClient">The client</param>
protected T AddApiClient<T>(T apiClient) where T : BaseApiClient
{
if (ClientOptions == null)
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
_logger.Log(LogLevel.Trace, " {ApiClient}, base address: {BaseAddress}", apiClient.GetType().Name, apiClient.BaseAddress);
ApiClients.Add(apiClient);
return apiClient;
}
/// <summary>
/// Apply the options delegate to a new options instance
/// </summary>
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
{
var opts = new T();
del?.Invoke(opts);
return opts;
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose(bool disposing)
{
if (disposing)
{
Exchange = exchange;
}
/// <summary>
/// Initialize the client with the specified options
/// </summary>
/// <param name="options"></param>
/// <exception cref="ArgumentNullException"></exception>
protected virtual void Initialize(ExchangeOptions options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
ClientOptions = options;
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
}
/// <summary>
/// Register an API client
/// </summary>
/// <param name="apiClient">The client</param>
protected T AddApiClient<T>(T apiClient) where T : BaseApiClient
{
if (ClientOptions == null)
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
ApiClients.Add(apiClient);
return apiClient;
}
/// <summary>
/// Apply the options delegate to a new options instance
/// </summary>
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
{
var opts = new T();
del?.Invoke(opts);
return opts;
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose()
{
Disposed = true;
_logger.Log(LogLevel.Debug, "Disposing client");
foreach (var client in ApiClients)
client.Dispose();
}
}
}
+18 -84
View File
@@ -1,91 +1,25 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using System.Linq;
using CryptoExchange.Net.Interfaces;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Collections.Generic;
using System.Linq;
namespace CryptoExchange.Net.Clients
namespace CryptoExchange.Net.Clients;
/// <summary>
/// Base rest client
/// </summary>
public abstract class BaseRestClient : BaseClient, IRestClient
{
/// <summary>
/// Base rest client
/// </summary>
public abstract class BaseRestClient : BaseClient, IRestClient
{
/// <summary>
/// Api clients in this client
/// </summary>
internal new List<RestApiClient> ApiClients => base.ApiClients.OfType<RestApiClient>().ToList();
/// <inheritdoc />
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
/// <summary>
/// ctor
/// </summary>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the API this client is for</param>
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
}
/// <summary>
/// Update options
/// </summary>
public virtual void SetOptions(UpdateOptions options)
{
foreach (var apiClient in ApiClients)
apiClient.SetOptions(options);
}
}
/// <inheritdoc />
public abstract class BaseRestClient<TEnvironment, TApiCredentials> : BaseRestClient, IRestClient<TApiCredentials>
where TEnvironment : TradeEnvironment
where TApiCredentials : ApiCredentials
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
/// <summary>
/// ctor
/// </summary>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the API this client is for</param>
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{
/// <summary>
/// Api clients in this client
/// </summary>
internal new List<RestApiClient<TEnvironment, TApiCredentials>> ApiClients => base.ApiClients.OfType<RestApiClient<TEnvironment, TApiCredentials>>().ToList();
/// <summary>
/// Provided client options
/// </summary>
public new RestExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (RestExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
/// <summary>
/// ctor
/// </summary>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the API this client is for</param>
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{
}
/// <summary>
/// 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)
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
}
/// <summary>
/// Update options
/// </summary>
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
{
foreach (var apiClient in ApiClients)
apiClient.SetOptions(options);
}
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
}
}
}
+113 -180
View File
@@ -1,197 +1,130 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace CryptoExchange.Net.Clients
namespace CryptoExchange.Net.Clients;
/// <summary>
/// Base for socket client implementations
/// </summary>
public abstract class BaseSocketClient : BaseClient, ISocketClient
{
#region fields
/// <summary>
/// Base for socket client implementations
/// If client is disposing
/// </summary>
public abstract class BaseSocketClient : BaseClient, ISocketClient
{
#region fields
/// <summary>
/// Api clients in this client
/// </summary>
internal new List<SocketApiClient> ApiClients => base.ApiClients.OfType<SocketApiClient>().ToList();
/// <summary>
/// If client is disposing
/// </summary>
protected bool _disposing;
/// <inheritdoc />
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
/// <inheritdoc />
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
/// <inheritdoc />
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
/// <inheritdoc />
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
#endregion
/// <summary>
/// ctor
/// </summary>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the exchange this client is for</param>
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
}
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
/// <returns></returns>
public virtual async Task UnsubscribeAsync(int subscriptionId)
{
foreach (var socket in ApiClients.OfType<SocketApiClient>())
{
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
if (result)
break;
}
}
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
/// <param name="subscription">The subscription to unsubscribe</param>
/// <returns></returns>
public virtual async Task UnsubscribeAsync(UpdateSubscription subscription)
{
if (subscription == null)
throw new ArgumentNullException(nameof(subscription));
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
await subscription.CloseAsync().ConfigureAwait(false);
}
/// <summary>
/// Unsubscribe all subscriptions
/// </summary>
/// <returns></returns>
public virtual async Task UnsubscribeAllAsync()
{
var tasks = new List<Task>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
tasks.Add(client.UnsubscribeAllAsync());
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
/// <summary>
/// Reconnect all connections
/// </summary>
/// <returns></returns>
public virtual async Task ReconnectAsync()
{
_logger.ReconnectingAllConnections(CurrentConnections);
var tasks = new List<Task>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
{
tasks.Add(client.ReconnectAsync());
}
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
/// <summary>
/// Log the current state of connections and subscriptions
/// </summary>
public string GetSubscriptionsState()
{
var result = new StringBuilder();
foreach (var client in ApiClients.OfType<SocketApiClient>().Where(c => c.CurrentSubscriptions > 0))
{
result.AppendLine(client.GetSubscriptionsState());
}
return result.ToString();
}
/// <summary>
/// Returns the state of all socket api clients
/// </summary>
/// <returns></returns>
public List<SocketApiClient.SocketApiClientState> GetSocketApiClientStates()
{
var result = new List<SocketApiClient.SocketApiClientState>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
{
result.Add(client.GetState());
}
return result;
}
/// <summary>
/// Update options
/// </summary>
public virtual void SetOptions(UpdateOptions options)
{
foreach (var apiClient in ApiClients)
apiClient.SetOptions(options);
}
}
protected bool _disposing;
/// <inheritdoc />
public abstract class BaseSocketClient<TEnvironment, TApiCredentials> : BaseSocketClient, ISocketClient<TApiCredentials>
where TEnvironment : TradeEnvironment
where TApiCredentials : ApiCredentials
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
/// <inheritdoc />
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
/// <inheritdoc />
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
#endregion
/// <summary>
/// ctor
/// </summary>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the exchange this client is for</param>
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{
/// <summary>
/// Api clients in this client
/// </summary>
internal new List<SocketApiClient<TEnvironment, TApiCredentials>> ApiClients => base.ApiClients.OfType<SocketApiClient<TEnvironment, TApiCredentials>>().ToList();
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
}
/// <summary>
/// Provided client options
/// </summary>
public new SocketExchangeOptions<TEnvironment, TApiCredentials> ClientOptions => (SocketExchangeOptions<TEnvironment, TApiCredentials>)base.ClientOptions;
/// <summary>
/// ctor
/// </summary>
/// <param name="loggerFactory">Logger factory</param>
/// <param name="name">The name of the API this client is for</param>
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
/// <returns></returns>
public virtual async Task UnsubscribeAsync(int subscriptionId)
{
foreach (var socket in ApiClients.OfType<SocketApiClient>())
{
}
/// <summary>
/// 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 virtual void SetApiCredentials(TApiCredentials credentials)
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
}
/// <summary>
/// Update options
/// </summary>
public virtual void SetOptions(UpdateOptions<TApiCredentials> options)
{
foreach (var apiClient in ApiClients)
apiClient.SetOptions(options);
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
if (result)
break;
}
}
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
/// <param name="subscription">The subscription to unsubscribe</param>
/// <returns></returns>
public virtual async Task UnsubscribeAsync(UpdateSubscription subscription)
{
if (subscription == null)
throw new ArgumentNullException(nameof(subscription));
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
await subscription.CloseAsync().ConfigureAwait(false);
}
/// <summary>
/// Unsubscribe all subscriptions
/// </summary>
/// <returns></returns>
public virtual async Task UnsubscribeAllAsync()
{
var tasks = new List<Task>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
tasks.Add(client.UnsubscribeAllAsync());
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
/// <summary>
/// Reconnect all connections
/// </summary>
/// <returns></returns>
public virtual async Task ReconnectAsync()
{
_logger.ReconnectingAllConnections(CurrentConnections);
var tasks = new List<Task>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
{
tasks.Add(client.ReconnectAsync());
}
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
}
/// <summary>
/// Log the current state of connections and subscriptions
/// </summary>
public string GetSubscriptionsState()
{
var result = new StringBuilder();
foreach (var client in ApiClients.OfType<SocketApiClient>().Where(c => c.CurrentSubscriptions > 0))
{
result.AppendLine(client.GetSubscriptionsState());
}
return result.ToString();
}
/// <summary>
/// Returns the state of all socket api clients
/// </summary>
/// <returns></returns>
public List<SocketApiClient.SocketApiClientState> GetSocketApiClientStates()
{
var result = new List<SocketApiClient.SocketApiClientState>();
foreach (var client in ApiClients.OfType<SocketApiClient>())
{
result.Add(client.GetState());
}
return result;
}
}
@@ -0,0 +1,78 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Clients;
/// <summary>
/// Base crypto client
/// </summary>
public class CryptoBaseClient : IDisposable
{
private readonly Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
/// <summary>
/// Service provider
/// </summary>
protected readonly IServiceProvider? _serviceProvider;
/// <summary>
/// ctor
/// </summary>
public CryptoBaseClient() { }
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoBaseClient(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_serviceCache = new Dictionary<Type, object>();
}
/// <summary>
/// Try get a client by type for the service collection
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T TryGet<T>(Func<T> createFunc)
{
var type = typeof(T);
if (_serviceCache.TryGetValue(type, out var value))
return (T)value;
if (_serviceProvider == null)
{
// Create with default options
var createResult = createFunc();
_serviceCache.Add(typeof(T), createResult!);
return createResult;
}
var result = _serviceProvider.GetService<T>()
?? throw new InvalidOperationException($"No service was found for {typeof(T).Name}, make sure the exchange is registered in dependency injection with the `services.Add[Exchange]()` method");
_serviceCache.Add(type, result!);
return result;
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose(bool disposing)
{
if (disposing)
{
_serviceCache.Clear();
}
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
@@ -0,0 +1,23 @@
using CryptoExchange.Net.Interfaces;
using System;
namespace CryptoExchange.Net.Clients;
/// <inheritdoc />
public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient
{
/// <summary>
/// ctor
/// </summary>
public CryptoRestClient()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
}
@@ -0,0 +1,23 @@
using CryptoExchange.Net.Interfaces;
using System;
namespace CryptoExchange.Net.Clients;
/// <inheritdoc />
public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient
{
/// <summary>
/// ctor
/// </summary>
public CryptoSocketClient()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoSocketClient(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,25 +1,24 @@
using System;
using System;
namespace CryptoExchange.Net.Converters
namespace CryptoExchange.Net.Converters;
/// <summary>
/// Mark property as an index in the array
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ArrayPropertyAttribute : Attribute
{
/// <summary>
/// Mark property as an index in the array
/// The index in the array
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ArrayPropertyAttribute : Attribute
{
/// <summary>
/// The index in the array
/// </summary>
public int Index { get; }
public int Index { get; }
/// <summary>
/// ctor
/// </summary>
/// <param name="index"></param>
public ArrayPropertyAttribute(int index)
{
Index = index;
}
/// <summary>
/// ctor
/// </summary>
/// <param name="index"></param>
public ArrayPropertyAttribute(int index)
{
Index = index;
}
}
@@ -1,29 +1,28 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters
namespace CryptoExchange.Net.Converters;
/// <summary>
/// Caching for JsonSerializerContext instances
/// </summary>
public static class JsonSerializerContextCache
{
private static ConcurrentDictionary<Type, JsonSerializerContext> _cache = new ConcurrentDictionary<Type, JsonSerializerContext>();
/// <summary>
/// Caching for JsonSerializerContext instances
/// Get the instance of the provided type T. It will be created if it doesn't exist yet.
/// </summary>
public static class JsonSerializerContextCache
/// <typeparam name="T">Implementation type of the JsonSerializerContext</typeparam>
public static JsonSerializerContext GetOrCreate<T>() where T: JsonSerializerContext, new()
{
private static ConcurrentDictionary<Type, JsonSerializerContext> _cache = new ConcurrentDictionary<Type, JsonSerializerContext>();
var contextType = typeof(T);
if (_cache.TryGetValue(contextType, out var context))
return context;
/// <summary>
/// Get the instance of the provided type T. It will be created if it doesn't exist yet.
/// </summary>
/// <typeparam name="T">Implementation type of the JsonSerializerContext</typeparam>
public static JsonSerializerContext GetOrCreate<T>() where T: JsonSerializerContext, new()
{
var contextType = typeof(T);
if (_cache.TryGetValue(contextType, out var context))
return context;
var instance = new T();
_cache[contextType] = instance;
return instance;
}
var instance = new T();
_cache[contextType] = instance;
return instance;
}
}
@@ -1,64 +0,0 @@
using CryptoExchange.Net.Objects;
using System.IO;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// REST message handler
/// </summary>
public interface IRestMessageHandler
{
/// <summary>
/// The `accept` HTTP response header for the request
/// </summary>
MediaTypeWithQualityHeaderValue AcceptHeader { get; }
/// <summary>
/// Whether a seekable stream is required
/// </summary>
bool RequiresSeekableStream { get; }
/// <summary>
/// Parse the response when the HTTP response status indicated an error
/// </summary>
ValueTask<Error> ParseErrorResponse(
int httpStatusCode,
HttpResponseHeaders responseHeaders,
Stream responseStream);
/// <summary>
/// Parse the response when the HTTP response status indicated a rate limit error
/// </summary>
ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
int httpStatusCode,
HttpResponseHeaders responseHeaders,
Stream responseStream);
/// <summary>
/// Check if the response is an error response; if so return the error.<br />
/// Note that if the API returns a standard result wrapper, something like this:
/// <code>{ "code": 400, "msg": "error", "data": {} }</code>
/// then the `CheckDeserializedResponse` method should be used for checking the result
/// </summary>
ValueTask<Error?> CheckForErrorResponse(
RequestDefinition request,
HttpResponseHeaders responseHeaders,
Stream responseStream);
/// <summary>
/// Deserialize the response stream
/// </summary>
ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(
Stream responseStream,
CancellationToken ct);
/// <summary>
/// Check whether the resulting T object indicates an error or not
/// </summary>
Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result);
}
}
@@ -1,27 +0,0 @@
using System;
using System.Net.WebSockets;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// WebSocket message handler
/// </summary>
public interface ISocketMessageHandler
{
/// <summary>
/// Get an identifier for the message which can be used to determine the type of the message
/// </summary>
string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType);
/// <summary>
/// Get optional topic filter, for example a symbol name
/// </summary>
string? GetTopicFilter(object deserializedObject);
/// <summary>
/// Deserialize to the provided type
/// </summary>
object Deserialize(ReadOnlySpan<byte> data, Type type);
}
}
@@ -1,46 +0,0 @@
using System;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// Message type definition
/// </summary>
public class MessageTypeDefinition
{
/// <summary>
/// Whether to immediately select the definition when it is matched. Can only be used when the evaluator has a single unique field to look for
/// </summary>
public bool ForceIfFound { get; set; }
/// <summary>
/// The fields a message needs to contain for this definition
/// </summary>
public MessageFieldReference[] Fields { get; set; } = [];
/// <summary>
/// The callback for getting the identifier string
/// </summary>
public Func<SearchResult, string>? TypeIdentifierCallback { get; set; }
/// <summary>
/// The static identifier string to return when this evaluator is matched
/// </summary>
public string? StaticIdentifier { get; set; }
internal string? GetMessageType(SearchResult result)
{
if (StaticIdentifier != null)
return StaticIdentifier;
return TypeIdentifierCallback!(result);
}
internal bool Satisfied(SearchResult result)
{
foreach(var field in Fields)
{
if (!result.Contains(field))
return false;
}
return true;
}
}
}
@@ -1,15 +0,0 @@
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
internal class MessageEvalutorFieldReference
{
public bool SkipReading { get; set; }
public bool OverlappingField { get; set; }
public MessageFieldReference Field { get; set; }
public MessageTypeDefinition? ForceEvaluator { get; set; }
public MessageEvalutorFieldReference(MessageFieldReference field)
{
Field = field;
}
}
}
@@ -1,152 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// Reference to a message field
/// </summary>
public abstract class MessageFieldReference
{
/// <summary>
/// The name for this search field
/// </summary>
public string SearchName { get; set; }
/// <summary>
/// The depth at which to look for this field
/// </summary>
public int Depth { get; set; } = 1;
/// <summary>
/// Callback to check if the field value matches an expected constraint
/// </summary>
public Func<string?, bool>? Constraint { get; private set; }
/// <summary>
/// Check whether the value is one of the string values in the set
/// </summary>
public MessageFieldReference WithFilterConstraint(HashSet<string?> set)
{
Constraint = set.Contains;
return this;
}
/// <summary>
/// Check whether the value is equal to a string
/// </summary>
public MessageFieldReference WithEqualConstraint(string compare)
{
Constraint = x => x != null && x.Equals(compare, StringComparison.Ordinal);
return this;
}
/// <summary>
/// Check whether the value is not equal to a string
/// </summary>
public MessageFieldReference WithNotEqualConstraint(string compare)
{
Constraint = x => x == null || !x.Equals(compare, StringComparison.Ordinal);
return this;
}
/// <summary>
/// Check whether the value is not null
/// </summary>
public MessageFieldReference WithNotNullConstraint()
{
Constraint = x => x != null;
return this;
}
/// <summary>
/// Check whether the value starts with a certain string
/// </summary>
public MessageFieldReference WithStartsWithConstraint(string start)
{
Constraint = x => x != null && x.StartsWith(start, StringComparison.Ordinal);
return this;
}
/// <summary>
/// Check whether the value starts with a certain string
/// </summary>
public MessageFieldReference WithStartsWithConstraints(params string[] startValues)
{
Constraint = x =>
{
if (x == null)
return false;
foreach (var item in startValues)
{
if (x!.StartsWith(item, StringComparison.Ordinal))
return true;
}
return false;
};
return this;
}
/// <summary>
/// Check whether the value starts with a certain string
/// </summary>
public MessageFieldReference WithCustomConstraint(Func<string?, bool> constraint)
{
Constraint = constraint;
return this;
}
/// <summary>
/// ctor
/// </summary>
public MessageFieldReference(string searchName)
{
SearchName = searchName;
}
}
/// <summary>
/// Reference to a property message field
/// </summary>
public class PropertyFieldReference : MessageFieldReference
{
/// <summary>
/// The property name in the JSON
/// </summary>
public byte[] PropertyName { get; set; }
/// <summary>
/// Whether the property value is array values
/// </summary>
public bool ArrayValues { get; set; }
/// <summary>
/// ctor
/// </summary>
public PropertyFieldReference(string propertyName) : base(propertyName)
{
PropertyName = Encoding.UTF8.GetBytes(propertyName);
}
}
/// <summary>
/// Reference to an array message field
/// </summary>
public class ArrayFieldReference : MessageFieldReference
{
/// <summary>
/// The index in the array
/// </summary>
public int ArrayIndex { get; set; }
/// <summary>
/// ctor
/// </summary>
public ArrayFieldReference(string searchName, int depth, int index) : base(searchName)
{
Depth = depth;
ArrayIndex = index;
}
}
}
@@ -1,60 +0,0 @@
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// The results of a search for fields in a JSON message
/// </summary>
public class SearchResult
{
private List<SearchResultItem> _items = new List<SearchResultItem>();
/// <summary>
/// Get the value of a field
/// </summary>
public string? FieldValue(string searchName)
{
foreach (var item in _items)
{
if (item.Field.SearchName.Equals(searchName, StringComparison.Ordinal))
return item.Value;
}
throw new Exception($"No field value found for {searchName}");
}
/// <summary>
/// The number of found search field values
/// </summary>
public int Count => _items.Count;
/// <summary>
/// Clear the search result
/// </summary>
public void Clear() => _items.Clear();
/// <summary>
/// Whether the value for a specific field was found
/// </summary>
public bool Contains(MessageFieldReference field)
{
foreach (var item in _items)
{
if (item.Field == field)
return true;
}
return false;
}
/// <summary>
/// Write a value to the result
/// </summary>
public void Write(MessageFieldReference field, string? value) => _items.Add(new SearchResultItem
{
Field = field,
Value = value
});
}
}
@@ -1,17 +0,0 @@
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// Search result value
/// </summary>
public struct SearchResultItem
{
/// <summary>
/// The field the values is for
/// </summary>
public MessageFieldReference Field { get; set; }
/// <summary>
/// The value of the field
/// </summary>
public string? Value { get; set; }
}
}
@@ -0,0 +1,48 @@
namespace CryptoExchange.Net.Converters.MessageParsing;
/// <summary>
/// Node accessor
/// </summary>
public readonly struct NodeAccessor
{
/// <summary>
/// Index
/// </summary>
public int? Index { get; }
/// <summary>
/// Property name
/// </summary>
public string? Property { get; }
/// <summary>
/// Type (0 = int, 1 = string, 2 = prop name)
/// </summary>
public int Type { get; }
private NodeAccessor(int? index, string? property, int type)
{
Index = index;
Property = property;
Type = type;
}
/// <summary>
/// Create an int node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); }
/// <summary>
/// Create a string node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); }
/// <summary>
/// Create a property name node accessor
/// </summary>
/// <returns></returns>
public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); }
}
@@ -0,0 +1,49 @@
using System.Collections;
using System.Collections.Generic;
namespace CryptoExchange.Net.Converters.MessageParsing;
/// <summary>
/// Message access definition
/// </summary>
public readonly struct MessagePath : IEnumerable<NodeAccessor>
{
private readonly List<NodeAccessor> _path;
internal void Add(NodeAccessor node)
{
_path.Add(node);
}
/// <summary>
/// ctor
/// </summary>
public MessagePath()
{
_path = new List<NodeAccessor>();
}
/// <summary>
/// Create a new message path
/// </summary>
/// <returns></returns>
public static MessagePath Get()
{
return new MessagePath();
}
/// <summary>
/// IEnumerable implementation
/// </summary>
/// <returns></returns>
public IEnumerator<NodeAccessor> GetEnumerator()
{
for (var i = 0; i < _path.Count; i++)
yield return _path[i];
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
@@ -0,0 +1,42 @@
namespace CryptoExchange.Net.Converters.MessageParsing;
/// <summary>
/// Message path extension methods
/// </summary>
public static class MessagePathExtension
{
/// <summary>
/// Add a string node accessor
/// </summary>
/// <param name="path"></param>
/// <param name="propName"></param>
/// <returns></returns>
public static MessagePath Property(this MessagePath path, string propName)
{
path.Add(NodeAccessor.String(propName));
return path;
}
/// <summary>
/// Add a property name node accessor
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static MessagePath PropertyName(this MessagePath path)
{
path.Add(NodeAccessor.PropertyName());
return path;
}
/// <summary>
/// Add a int node accessor
/// </summary>
/// <param name="path"></param>
/// <param name="index"></param>
/// <returns></returns>
public static MessagePath Index(this MessagePath path, int index)
{
path.Add(NodeAccessor.Int(index));
return path;
}
}
@@ -0,0 +1,20 @@
namespace CryptoExchange.Net.Converters.MessageParsing;
/// <summary>
/// Message node type
/// </summary>
public enum NodeType
{
/// <summary>
/// Array node
/// </summary>
Array,
/// <summary>
/// Object node
/// </summary>
Object,
/// <summary>
/// Value node
/// </summary>
Value
}
@@ -1,246 +1,232 @@
using CryptoExchange.Net.Exceptions;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Collections.Generic;
#if NET5_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif
using System.Threading;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
/// with [ArrayProperty(x)] where x is the index of the property in the array
/// </summary>
#if NET5_0_OR_GREATER
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter<T> where T : new()
#else
public class ArrayConverter<T> : JsonConverter<T> where T : new()
#endif
{
private static SortedDictionary<int, List<ArrayPropertyInfo>>? _typePropertyInfo;
namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <inheritdoc />
/// <summary>
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
/// with [ArrayProperty(x)] where x is the index of the property in the array
/// </summary>
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter<T> where T : new()
#else
public class ArrayConverter<T> : JsonConverter<T> where T : new()
#endif
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
private static readonly Lazy<List<ArrayPropertyInfo>> _typePropertyInfo = new Lazy<List<ArrayPropertyInfo>>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly);
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value == null)
{
if (value == null)
writer.WriteNullValue();
return;
}
writer.WriteStartArray();
var ordered = _typePropertyInfo.Value.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
var last = -1;
foreach (var prop in ordered)
{
if (prop.ArrayProperty.Index == last)
continue;
while (prop.ArrayProperty.Index != last + 1)
{
writer.WriteNullValue();
return;
last += 1;
}
if (_typePropertyInfo == null)
_typePropertyInfo = CacheTypeAttributes();
last = prop.ArrayProperty.Index;
writer.WriteStartArray();
var last = -1;
foreach (var indexProps in _typePropertyInfo)
var objValue = prop.PropertyInfo.GetValue(value);
if (objValue == null)
{
foreach (var prop in indexProps.Value)
writer.WriteNullValue();
continue;
}
JsonSerializerOptions? typeOptions = null;
if (prop.JsonConverter != null)
{
typeOptions = new JsonSerializerOptions
{
if (prop.ArrayProperty.Index == last)
// Don't write the same index twice
continue;
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
TypeInfoResolver = options.TypeInfoResolver,
};
typeOptions.Converters.Add(prop.JsonConverter);
}
while (prop.ArrayProperty.Index != last + 1)
if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType))
{
if (prop.TargetType == typeof(string))
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
else if (prop.TargetType == typeof(bool))
writer.WriteBooleanValue((bool)objValue);
else
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
}
else
{
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
}
}
writer.WriteEndArray();
}
/// <inheritdoc />
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return default;
var result = new T();
return ParseObject(ref reader, result, options);
}
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
#else
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
#endif
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new Exception("Not an array");
int index = 0;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
var indexAttributes = _typePropertyInfo.Value.Where(a => a.ArrayProperty.Index == index);
if (!indexAttributes.Any())
{
index++;
continue;
}
foreach (var attribute in indexAttributes)
{
var targetType = attribute.TargetType;
object? value = null;
if (attribute.JsonConverter != null)
{
if (attribute.JsonSerializerOptions == null)
{
writer.WriteNullValue();
last += 1;
}
last = prop.ArrayProperty.Index;
var objValue = prop.PropertyInfo.GetValue(value);
if (objValue == null)
{
writer.WriteNullValue();
continue;
}
JsonSerializerOptions? typeOptions = null;
if (prop.JsonConverter != null)
{
typeOptions = new JsonSerializerOptions
attribute.JsonSerializerOptions = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
Converters = { attribute.JsonConverter },
TypeInfoResolver = options.TypeInfoResolver,
};
typeOptions.Converters.Add(prop.JsonConverter);
}
if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType))
{
if (prop.TargetType == typeof(string))
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
else if (prop.TargetType == typeof(bool))
writer.WriteBooleanValue((bool)objValue);
else
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
}
else
{
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
}
var doc = JsonDocument.ParseValue(ref reader);
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions);
}
else if (attribute.DefaultDeserialization)
{
value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType));
}
else
{
value = reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.False => false,
JsonTokenType.True => true,
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetDecimal(),
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
};
}
if (targetType.IsAssignableFrom(value?.GetType()))
attribute.PropertyInfo.SetValue(result, value);
else
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
}
writer.WriteEndArray();
index++;
}
/// <inheritdoc />
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
return result;
}
private static bool IsSimple(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
if (reader.TokenType == JsonTokenType.Null)
return default;
var result = new T();
return ParseObject(ref reader, result, options);
// nullable type, check if the nested type is simple.
return IsSimple(type.GetGenericArguments()[0]);
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
}
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
private static List<ArrayPropertyInfo> CacheTypeAttributes()
#else
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
private static List<ArrayPropertyInfo> CacheTypeAttributes()
#endif
{
var attributes = new List<ArrayPropertyInfo>();
var properties = typeof(T).GetProperties();
foreach (var property in properties)
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new CeDeserializationException("Not an array");
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
if (att == null)
continue;
if (_typePropertyInfo == null)
_typePropertyInfo = CacheTypeAttributes();
int index = 0;
while (reader.Read())
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
attributes.Add(new ArrayPropertyInfo
{
if (reader.TokenType == JsonTokenType.EndArray)
break;
if(!_typePropertyInfo.TryGetValue(index, out var indexAttributes))
{
index++;
continue;
}
foreach (var attribute in indexAttributes)
{
var targetType = attribute.TargetType;
object? value = null;
if (attribute.JsonConverter != null)
{
if (attribute.JsonSerializerOptions == null)
{
attribute.JsonSerializerOptions = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
PropertyNameCaseInsensitive = false,
Converters = { attribute.JsonConverter },
TypeInfoResolver = options.TypeInfoResolver,
};
}
var doc = JsonDocument.ParseValue(ref reader);
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions);
}
else if (attribute.DefaultDeserialization)
{
value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType));
}
else
{
value = reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.False => false,
JsonTokenType.True => true,
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetDecimal(),
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
_ => throw new CeDeserializationException($"Array deserialization of type {reader.TokenType} not supported"),
};
}
if (targetType.IsAssignableFrom(value?.GetType()))
attribute.PropertyInfo.SetValue(result, value);
else
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
}
index++;
}
return result;
ArrayProperty = att,
PropertyInfo = property,
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
TargetType = targetType
});
}
private static bool IsSimple(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// nullable type, check if the nested type is simple.
return IsSimple(type.GetGenericArguments()[0]);
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
}
return attributes;
}
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes()
#else
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes()
#endif
{
var result = new SortedDictionary<int, List<ArrayPropertyInfo>>();
var properties = typeof(T).GetProperties();
foreach (var property in properties)
{
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
if (att == null)
continue;
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
if (!result.TryGetValue(att.Index, out var indexList))
{
indexList = new List<ArrayPropertyInfo>();
result[att.Index] = indexList;
}
indexList.Add(new ArrayPropertyInfo
{
ArrayProperty = att,
PropertyInfo = property,
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
TargetType = targetType
});
}
return result;
}
private class ArrayPropertyInfo
{
public PropertyInfo PropertyInfo { get; set; } = null!;
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
public JsonConverter? JsonConverter { get; set; }
public bool DefaultDeserialization { get; set; }
public Type TargetType { get; set; } = null!;
public JsonSerializerOptions? JsonSerializerOptions { get; set; } = null;
}
private class ArrayPropertyInfo
{
public PropertyInfo PropertyInfo { get; set; } = null!;
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
public JsonConverter? JsonConverter { get; set; }
public bool DefaultDeserialization { get; set; }
public Type TargetType { get; set; } = null!;
public JsonSerializerOptions? JsonSerializerOptions { get; set; }
}
}
@@ -1,46 +1,45 @@
using System;
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
/// </summary>
public class BigDecimalConverter : JsonConverter<decimal>
{
/// <inheritdoc />
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
try
{
return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
}
catch(OverflowException)
{
// Value doesn't fit decimal, default to max value
return decimal.MaxValue;
}
}
namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
/// </summary>
public class BigDecimalConverter : JsonConverter<decimal>
{
/// <inheritdoc />
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
try
{
return reader.GetDecimal();
return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
}
catch(FormatException)
catch(OverflowException)
{
// Format issue, assume value is too large
// Value doesn't fit decimal, default to max value
return decimal.MaxValue;
}
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
try
{
writer.WriteNumberValue(value);
return reader.GetDecimal();
}
catch(FormatException)
{
// Format issue, assume value is too large
return decimal.MaxValue;
}
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
{
writer.WriteNumberValue(value);
}
}
@@ -1,54 +1,34 @@
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// Bool converter
/// </summary>
public class BoolConverter : JsonConverterFactory
{
/// <summary>
/// Bool converter
/// </summary>
public class BoolConverter : JsonConverterFactory
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(bool) || typeToConvert == typeof(bool?);
}
return typeToConvert == typeof(bool) || typeToConvert == typeof(bool?);
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return typeToConvert == typeof(bool) ? new BoolConverterInner() : new BoolConverterInnerNullable();
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return typeToConvert == typeof(bool) ? new BoolConverterInner<bool>() : new BoolConverterInner<bool?>();
}
private class BoolConverterInnerNullable : JsonConverter<bool?>
{
public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadBool(ref reader, typeToConvert, options);
public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options)
{
if (value is bool boolVal)
writer.WriteBooleanValue(boolVal);
else
writer.WriteNullValue();
}
}
private class BoolConverterInner : JsonConverter<bool>
{
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadBool(ref reader, typeToConvert, options) ?? false;
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteBooleanValue(value);
}
}
private static bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
private class BoolConverterInner<T> : JsonConverter<T>
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> (T)((object?)ReadBool(ref reader, typeToConvert, options) ?? default(T))!;
public static bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.True)
return true;
@@ -67,7 +47,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (string.IsNullOrEmpty(value))
{
if (typeToConvert == typeof(bool))
LibraryHelpers.StaticLogger?.LogWarning("Received null or empty bool value, but property type is not a nullable bool. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null bool value, but property type is not a nullable bool");
return default;
}
@@ -91,5 +71,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
throw new SerializationException($"Can't convert bool value {value}");
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value is bool boolVal)
writer.WriteBooleanValue(boolVal);
else
writer.WriteNullValue();
}
}
}
@@ -1,35 +1,36 @@
using System;
using System;
#if NET5_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for comma separated enum values
/// </summary>
namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// Converter for comma separated enum values
/// </summary>
#if NET5_0_OR_GREATER
public class CommaSplitEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter<T[]> where T : struct, Enum
public class CommaSplitEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter<T[]> where T : struct, Enum
#else
public class CommaSplitEnumConverter<T> : JsonConverter<T[]> where T : struct, Enum
public class CommaSplitEnumConverter<T> : JsonConverter<T[]> where T : struct, Enum
#endif
{
/// <inheritdoc />
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
/// <inheritdoc />
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var str = reader.GetString();
if (string.IsNullOrEmpty(str))
return [];
var str = reader.GetString();
if (string.IsNullOrEmpty(str))
return [];
return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? [];
}
return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? [];
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
{
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
{
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
}
}
@@ -1,30 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for comma separated string values
/// </summary>
public class CommaSplitStringConverter : JsonConverter<string[]>
{
/// <inheritdoc />
public override string[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var str = reader.GetString();
if (string.IsNullOrEmpty(str))
return [];
return str!.Split(',').ToArray() ?? [];
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string[] value, JsonSerializerOptions options)
{
writer.WriteStringValue(string.Join(",", value));
}
}
}
@@ -1,98 +1,68 @@
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// Date time converter
/// </summary>
public class DateTimeConverter : JsonConverterFactory
{
/// <summary>
/// Date time converter
/// </summary>
public class DateTimeConverter : JsonConverterFactory
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d;
private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000;
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
}
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner<DateTime>() : new DateTimeConverterInner<DateTime?>();
}
/// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new NullableDateTimeConverterInner();
}
private class NullableDateTimeConverterInner : JsonConverter<DateTime?>
{
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadDateTime(ref reader, typeToConvert, options);
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
return;
}
if (value.Value == default)
writer.WriteStringValue(default(DateTime));
else
writer.WriteNumberValue((long)Math.Round((value.Value - new DateTime(1970, 1, 1)).TotalMilliseconds));
}
}
private class DateTimeConverterInner : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadDateTime(ref reader, typeToConvert, options) ?? default;
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
var dtValue = value;
if (dtValue == default)
writer.WriteStringValue(default(DateTime));
else
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
}
}
private class DateTimeConverterInner<T> : JsonConverter<T>
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> (T)((object?)ReadDateTime(ref reader, typeToConvert, options) ?? default(T))!;
private static DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
if (typeToConvert == typeof(DateTime))
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | DateTime value of null, but property is not nullable");
return default;
}
if (reader.TokenType is JsonTokenType.Number)
{
var decValue = reader.GetDecimal();
if (decValue == 0 || decValue < 0)
var longValue = reader.GetDouble();
if (longValue == 0 || longValue < 0)
return default;
return ParseFromDecimal(decValue);
return ParseFromDouble(longValue);
}
else if (reader.TokenType is JsonTokenType.String)
{
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 == "-1"
|| stringValue == "0001-01-01T00:00:00Z"
|| double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
{
return default;
}
return ParseFromString(stringValue!, options.TypeInfoResolver?.GetType()?.Name);
return ParseFromString(stringValue!);
}
else
{
@@ -100,174 +70,172 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}
}
/// <summary>
/// Parse a double value to datetime
/// </summary>
public static DateTime ParseFromDouble(double value)
=> ParseFromDecimal((decimal)value);
/// <summary>
/// Parse a decimal value to datetime
/// </summary>
public static DateTime ParseFromDecimal(decimal value)
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (value < 19999999999)
return ConvertFromSeconds(value);
if (value < 19999999999999)
return ConvertFromMilliseconds(value);
if (value < 19999999999999999)
return ConvertFromMicroseconds(value);
return ConvertFromNanoseconds(value);
if (value == null)
{
writer.WriteNullValue();
}
else
{
var dtValue = (DateTime)(object)value;
if (dtValue == default)
writer.WriteStringValue(default(DateTime));
else
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
}
}
/// <summary>
/// Parse a string value to datetime
/// </summary>
public static DateTime ParseFromString(string stringValue, string? resolverName)
{
if (stringValue!.Length == 12 && stringValue.StartsWith("202", StringComparison.OrdinalIgnoreCase))
{
// Parse 202303261200 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
{
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
return default;
}
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
}
if (stringValue.Length == 8)
{
// Parse 20211103 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
{
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (stringValue.Length == 6)
{
// Parse 211103 format
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
{
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
return default;
}
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (decimal.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var decimalValue))
{
// Parse 1637745563.000 format
if (decimalValue <= 0)
return default;
if (decimalValue < 19999999999)
return ConvertFromSeconds(decimalValue);
if (decimalValue < 19999999999999)
return ConvertFromMilliseconds(decimalValue);
if (decimalValue < 19999999999999999)
return ConvertFromMicroseconds(decimalValue);
return ConvertFromNanoseconds(decimalValue);
}
if (stringValue.Length == 10)
{
// Parse 2021-11-03 format
var values = stringValue.Split('-');
if (!int.TryParse(values[0], out var year)
|| !int.TryParse(values[1], out var month)
|| !int.TryParse(values[2], out var day))
{
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
}
/// <summary>
/// Convert a seconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromSeconds(decimal seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromSeconds(double seconds) => ConvertFromSeconds((decimal)seconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromSeconds(long seconds) => ConvertFromSeconds((decimal)seconds);
/// <summary>
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMilliseconds(decimal milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMilliseconds(double milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMilliseconds(long milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
/// <summary>
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMicroseconds(decimal microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMicroseconds(double microseconds) => ConvertFromMicroseconds((decimal)microseconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromMicroseconds(long microseconds) => ConvertFromMicroseconds((decimal)microseconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromNanoseconds(decimal nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromNanoseconds(double nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
public static DateTime ConvertFromNanoseconds(long nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
/// <summary>
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
/// </summary>
[return: NotNullIfNotNull("time")]
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
/// <summary>
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
/// </summary>
[return: NotNullIfNotNull("time")]
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
/// <summary>
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
/// </summary>
[return: NotNullIfNotNull("time")]
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
/// <summary>
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
/// </summary>
[return: NotNullIfNotNull("time")]
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
}
/// <summary>
/// Parse a long value to datetime
/// </summary>
/// <param name="longValue"></param>
/// <returns></returns>
public static DateTime ParseFromDouble(double longValue)
{
if (longValue < 19999999999)
return ConvertFromSeconds(longValue);
if (longValue < 19999999999999)
return ConvertFromMilliseconds(longValue);
if (longValue < 19999999999999999)
return ConvertFromMicroseconds(longValue);
return ConvertFromNanoseconds(longValue);
}
/// <summary>
/// Parse a string value to datetime
/// </summary>
/// <param name="stringValue"></param>
/// <returns></returns>
public static DateTime ParseFromString(string stringValue)
{
if (stringValue!.Length == 12 && stringValue.StartsWith("202"))
{
// Parse 202303261200 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
{
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
}
if (stringValue.Length == 8)
{
// Parse 20211103 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
{
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (stringValue.Length == 6)
{
// Parse 211103 format
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
{
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
{
// Parse 1637745563.000 format
if (doubleValue <= 0)
return default;
if (doubleValue < 19999999999)
return ConvertFromSeconds(doubleValue);
if (doubleValue < 19999999999999)
return ConvertFromMilliseconds((long)doubleValue);
if (doubleValue < 19999999999999999)
return ConvertFromMicroseconds((long)doubleValue);
return ConvertFromNanoseconds((long)doubleValue);
}
if (stringValue.Length == 10)
{
// Parse 2021-11-03 format
var values = stringValue.Split('-');
if (!int.TryParse(values[0], out var year)
|| !int.TryParse(values[1], out var month)
|| !int.TryParse(values[2], out var day))
{
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default;
}
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
}
/// <summary>
/// Convert a seconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="seconds"></param>
/// <returns></returns>
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
/// <summary>
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="milliseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
/// <summary>
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="microseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromMicroseconds(double microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
/// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary>
/// <param name="nanoseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromNanoseconds(double nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
/// <summary>
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
/// <summary>
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
/// <summary>
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
/// <summary>
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")]
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
}
@@ -1,44 +1,43 @@
using System;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <summary>
/// Decimal converter
/// </summary>
public class DecimalConverter : JsonConverter<decimal?>
{
/// <summary>
/// Decimal converter
/// </summary>
public class DecimalConverter : JsonConverter<decimal?>
/// <inheritdoc />
public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
/// <inheritdoc />
public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
if (reader.TokenType == JsonTokenType.Null)
return null;
if (reader.TokenType == JsonTokenType.String)
{
if (reader.TokenType == JsonTokenType.Null)
return null;
if (reader.TokenType == JsonTokenType.String)
{
var value = reader.GetString();
return ExchangeHelpers.ParseDecimal(value);
}
try
{
return reader.GetDecimal();
}
catch(FormatException)
{
// Format issue, assume value is too large
return decimal.MaxValue;
}
var value = reader.GetString();
return ExchangeHelpers.ParseDecimal(value);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options)
try
{
if (value == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.Value);
return reader.GetDecimal();
}
catch(FormatException)
{
// Format issue, assume value is too large
return decimal.MaxValue;
}
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options)
{
if (value == null)
writer.WriteNullValue();
else
writer.WriteNumberValue(value.Value);
}
}
@@ -1,23 +1,22 @@
using System;
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary>
/// Converter for serializing decimal values as string
/// </summary>
public class DecimalStringWriterConverter : JsonConverter<decimal>
{
/// <inheritdoc />
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
namespace CryptoExchange.Net.Converters.SystemTextJson;
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture) ?? null);
/// <summary>
/// Converter for serializing decimal values as string
/// </summary>
public class DecimalStringWriterConverter : JsonConverter<decimal>
{
/// <inheritdoc />
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture) ?? null);
}

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