mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-11-03 16:57:44 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 600131d14d | |||
| a092277036 | |||
| ae11dd382f | |||
| cbe21a86c7 | |||
| e45fa235e0 | |||
| f88ed02735 | |||
| 2ef4dd8555 | |||
| 7338513f81 | |||
| 11ac7e4f0b | |||
| a4ae3c6d87 | |||
| 3015da911e | |||
| 6df3dcc422 | |||
| 9eceaf1eea | |||
| f1ad4e9920 | |||
| ee53dfe139 | |||
| 186f30d4b5 | |||
| f079e6f0f4 | |||
| c6647f6815 | |||
| 5135047097 | |||
| 4f25a6e7d6 | |||
| a0f3dcaa4c | |||
| 550afd41c3 | |||
| afdced3bb8 | |||
| f8c657f308 | |||
| c9ae1bf379 | |||
| 59acbd648e | |||
| 7a11bd0c5e | |||
| 9502db6d58 | |||
| e6ab899654 | |||
| a8517d2c76 | |||
| bda81d8170 | |||
| 666ca76776 | |||
| ae7504c26c |
@@ -1,87 +1,92 @@
|
||||
---
|
||||
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.
|
||||
description: Conventions for cross-exchange C# code using CryptoExchange.Net Shared API V2 capabilities.
|
||||
globs:
|
||||
- "**/*.cs"
|
||||
- "**/*.csproj"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# CryptoExchange.Net Conventions
|
||||
# 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.
|
||||
CryptoExchange.Net is the base dependency for exchange-specific libraries. Install Binance.Net, Bybit.Net, JK.OKX.Net, or another exchange library; use `CryptoClients.Net` when the application needs the bundle.
|
||||
|
||||
## Multi-exchange pattern
|
||||
## Use Shared API V2
|
||||
|
||||
For new cross-exchange code, use the fine-grained interfaces in `CryptoExchange.Net.SharedApis` and each API surface's `.SharedApi` property:
|
||||
|
||||
```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;
|
||||
IGetTickerRest binance = new BinanceRestClient().SpotApi.SharedApi;
|
||||
IGetTickerRest okx = new OKXRestClient().UnifiedApi.SharedApi;
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
|
||||
var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||
// ticker.Data.LastPrice — same model regardless of exchange
|
||||
var result = await binance.GetTickerAsync(new GetTickerRequest(symbol));
|
||||
```
|
||||
|
||||
## Symbol normalization
|
||||
Use one capability per operation, such as `IGetTickerRest`, `IGetOrderBookRest`, `IPlaceSpotOrderRest`, or `ISubscribeTradesSocket`. Do not use a broad V1 interface for new code. `.SharedClient` remains only for incremental V1 migration.
|
||||
|
||||
`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.
|
||||
## Capability discovery
|
||||
|
||||
## Symbol metadata and catalogs
|
||||
|
||||
In 12.2.0, `SharedSpotSymbol` and `SharedFuturesSymbol` include `DisplayName` and base/quote asset classification through `SharedAssetType` (`Crypto`, `Fiat`, `TradFi`) and `SharedAssetSubType` (`StableCoin`, `Equity`, `Commodity`). Pass the matching base/quote filters to `GetSymbolsRequest` when discovery should return only a class of markets.
|
||||
|
||||
After calling `GetSpotSymbolsAsync`, `ISpotSymbolRestClient.SpotSymbolCatalog` maps asset and symbol names to shared metadata. `IFuturesSymbolRestClient.FuturesSymbolCatalog` works the same way after `GetFuturesSymbolsAsync`. Treat either property as unavailable before its corresponding request has populated the cache.
|
||||
|
||||
When implementing an exchange library, use `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` only as best-effort classifiers and supply exchange-specific additions where needed.
|
||||
|
||||
## Shared market-data quantities
|
||||
|
||||
In 12.4.0, use `SharedOrderQuantity`-valued `Volumes` on shared spot/futures tickers and klines, and `Quantities` on shared trades. The scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
|
||||
|
||||
## WebSocket order commands
|
||||
|
||||
In 12.5.0, `ISpotOrderManagementSocketClient` and `IFuturesOrderManagementSocketClient` optionally provide place/cancel order commands over WebSocket. Their methods return `QueryResult<SharedId>`, not `WebSocketResult<UpdateSubscription>`. Check whether the exchange implements the interface.
|
||||
|
||||
## Result pattern
|
||||
|
||||
REST methods return `HttpResult<T>` and websocket subscription methods return `WebSocketResult<UpdateSubscription>`. Always check `.Success`. `.Exchange` property identifies which exchange responded — useful for logging.
|
||||
|
||||
## Available shared interfaces
|
||||
|
||||
- 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, and optional spot/futures order management
|
||||
|
||||
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.
|
||||
When the exchange and API surface are known, assign the typed `.SharedApi` to the required capability. When selection is dynamic, use an exchange-wide `I[Exchange]SharedApiClient` and a strongly typed reference:
|
||||
|
||||
```csharp
|
||||
var clients = new ISpotTickerRestClient[] { binance, okx, bybit };
|
||||
var tasks = clients.Select(c => c.GetSpotTickerAsync(new GetTickerRequest(symbol)));
|
||||
var match = sharedClient.GetCapability(
|
||||
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
|
||||
TradingMode.PerpetualLinear);
|
||||
|
||||
if (match is not null)
|
||||
await match.Capability.PlaceFuturesOrderAsync(request);
|
||||
```
|
||||
|
||||
`GetCapability` returns one preferred match or `null`; `GetCapabilities` returns all matches. Specify `TradingMode` when multiple API surfaces may implement the same capability. A `SharedCapabilities` reference is a lookup key, not proof of exchange support.
|
||||
|
||||
## Symbols and parameters
|
||||
|
||||
Use `SharedSymbol` instead of native strings such as `BTCUSDT`. The exchange library performs formatting.
|
||||
|
||||
Capability presence does not imply support for every request property. For dynamic code, inspect `match.Options.RequestParameterRules`, `ExchangeParameterRules`, and `SupportedTradingModes`. Supply exchange-specific values through the request's `ExchangeParameters`.
|
||||
|
||||
## Transports and results
|
||||
|
||||
- Transport-agnostic capabilities return `IExchangeCallResult<T>`.
|
||||
- `...Rest` capabilities return `HttpResult<T>`.
|
||||
- socket command capabilities return `QueryResult<T>`.
|
||||
- `ISubscribe...Socket` capabilities return `WebSocketResult<UpdateSubscription>`.
|
||||
|
||||
Use `.Rest` or `.Socket` on capability references when the transport matters. Otherwise exchange-wide lookup honors `PreferredTransport`, normally REST. Always check `.Success` before `.Data` and include `.Exchange` in multi-exchange logs.
|
||||
|
||||
## Concurrent aggregation
|
||||
|
||||
```csharp
|
||||
var clients = new IGetTickerRest[] { binance, okx, bybit };
|
||||
var tasks = clients.Select(x =>
|
||||
x.GetTickerAsync(new GetTickerRequest(symbol)));
|
||||
var results = await Task.WhenAll(tasks);
|
||||
```
|
||||
|
||||
Reuse clients through dependency injection and run independent exchange calls concurrently.
|
||||
|
||||
## V2 semantics
|
||||
|
||||
- `GetTickerAsync` and `GetAllTickersAsync` return `SharedTicker` for spot and futures.
|
||||
- WebSocket order updates use `SharedSpotOrderUpdate` and `SharedFuturesOrderUpdate`.
|
||||
- `ICloseFullPosition` closes the entire position; use an order capability for partial closes.
|
||||
- Each exchange supports a subset of capabilities and parameter combinations.
|
||||
|
||||
## 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
|
||||
- Never install CryptoExchange.Net alone and expect exchange endpoints.
|
||||
- Never pass exchange-native models or symbols through shared services.
|
||||
- Never assume a capability or request field is supported; resolve and inspect it.
|
||||
- Never use `.Result` or `.Wait()`.
|
||||
- Never instantiate exchange clients per request.
|
||||
- Prefer `Task.WhenAll` for independent cross-exchange calls.
|
||||
- Prefer `.SharedApi` and fine-grained V2 interfaces for new code.
|
||||
|
||||
## 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`)
|
||||
- `AGENTS.md` for fuller examples
|
||||
- `docs/ai-api-map.md` for the V2 interface map
|
||||
- `docs/SHARED_API_V2_MIGRATION.md` for V1 migration details
|
||||
- `llms-full.txt` for expanded AI context
|
||||
|
||||
@@ -1,66 +1,70 @@
|
||||
# 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.).
|
||||
CryptoExchange.Net is the base library behind exchange-specific C#/.NET clients such as Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, and Coinbase.Net.
|
||||
|
||||
When generating code in this ecosystem, follow these conventions:
|
||||
## Package choice
|
||||
|
||||
## You don't install CryptoExchange.Net directly
|
||||
Do not install CryptoExchange.Net alone to call an exchange. Install the exchange-specific package, or use `CryptoClients.Net` for the full bundle. For single-exchange code, prefer that library's native client. For portable multi-exchange code, use `CryptoExchange.Net.SharedApis`.
|
||||
|
||||
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.
|
||||
## Generate Shared API V2 code
|
||||
|
||||
## 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:
|
||||
Use fine-grained V2 capabilities through `.SharedApi`:
|
||||
|
||||
```csharp
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
|
||||
ISpotTickerRestClient binance = new BinanceRestClient().SpotApi.SharedClient;
|
||||
ISpotTickerRestClient okx = new OKXRestClient().UnifiedApi.SharedClient;
|
||||
IGetTickerRest binance = new BinanceRestClient().SpotApi.SharedApi;
|
||||
IGetTickerRest okx = new OKXRestClient().UnifiedApi.SharedApi;
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||
var result = await binance.GetTickerAsync(new GetTickerRequest(symbol));
|
||||
```
|
||||
|
||||
Same code works on every exchange that implements the interface. Use `Task.WhenAll` for concurrent multi-exchange calls.
|
||||
Choose the interface for the operation: for example, `IGetTickerRest`, `IGetOrderBookRest`, `IPlaceSpotOrderRest`, `ICancelFuturesOrderRest`, or `ISubscribeTradesSocket`. V1 broad interfaces remain on `.SharedClient` for migration, but new code should use `.SharedApi`.
|
||||
|
||||
## Shared symbol metadata
|
||||
Use `SharedSymbol`; do not hard-code exchange-native symbol formatting in shared code.
|
||||
|
||||
CryptoExchange.Net 12.2.0 classifies the base and quote sides of `SharedSpotSymbol` and `SharedFuturesSymbol` with `SharedAssetType` (`Crypto`, `Fiat`, `TradFi`) and optional `SharedAssetSubType` (`StableCoin`, `Equity`, `Commodity`). The models also expose `DisplayName`. Use the corresponding base/quote fields on `GetSymbolsRequest` to filter symbol discovery.
|
||||
## Runtime capability selection
|
||||
|
||||
`ISpotSymbolRestClient.SpotSymbolCatalog` is populated by `GetSpotSymbolsAsync`; `IFuturesSymbolRestClient.FuturesSymbolCatalog` is populated by `GetFuturesSymbolsAsync`. Do not assume a catalog is available before that request. For exchange-library implementations, `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` offer best-effort classification and can be extended with exchange-specific values.
|
||||
When the API surface is known, assign its typed `.SharedApi` directly. When support or the API surface is selected at runtime, use the exchange-wide `I[Exchange]SharedApiClient`:
|
||||
|
||||
## Shared market-data quantities
|
||||
```csharp
|
||||
var match = sharedClient.GetCapability(
|
||||
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
|
||||
TradingMode.PerpetualLinear);
|
||||
|
||||
CryptoExchange.Net 12.4.0 uses `SharedOrderQuantity` for market-data quantities. Prefer `Volumes` on `SharedSpotTicker`, `SharedFuturesTicker`, and `SharedKline`, and `Quantities` on `SharedTrade`; the scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
|
||||
if (match is null)
|
||||
return;
|
||||
|
||||
## WebSocket order commands
|
||||
var result = await match.Capability.PlaceFuturesOrderAsync(request);
|
||||
```
|
||||
|
||||
CryptoExchange.Net 12.5.0 adds optional `ISpotOrderManagementSocketClient` and `IFuturesOrderManagementSocketClient` interfaces for placing and canceling orders over WebSocket. These command methods return `QueryResult<SharedId>` rather than a subscription result. Check exchange support before relying on either interface.
|
||||
`GetCapability` returns one preferred `SharedCapabilityResolution<T>` or `null`; `GetCapabilities` returns all matching implementations. Include `TradingMode` when an exchange can expose multiple futures surfaces. `SharedCapabilities` entries are lookup references, not guarantees of support.
|
||||
|
||||
## Single-exchange code uses the exchange's own client
|
||||
Before constructing dynamic requests, inspect `match.Options.RequestParameterRules`, `ExchangeParameterRules`, and `SupportedTradingModes`. A capability may exist while a particular request field is unsupported.
|
||||
|
||||
For Binance-only code, use `BinanceRestClient` directly (see Binance.Net repo `AGENTS.md`). SharedApis is for portability — use it when you need that.
|
||||
## Results and transports
|
||||
|
||||
## Result pattern
|
||||
- Transport-agnostic operation: `IExchangeCallResult<T>`
|
||||
- REST capability: `HttpResult<T>`
|
||||
- WebSocket command capability: `QueryResult<T>`
|
||||
- WebSocket subscription capability: `WebSocketResult<UpdateSubscription>`
|
||||
|
||||
REST methods return `HttpResult<T>` and websocket subscription methods return `WebSocketResult<UpdateSubscription>`. Check `.Success` before `.Data`. `.Error` has structured info. `.Exchange` on shared clients identifies which exchange responded.
|
||||
Select `.Rest` or `.Socket` when transport-specific behavior matters. Otherwise exchange-wide selection uses `PreferredTransport`, normally REST. Always check `.Success` before `.Data`; use `.Error` and `.Exchange` for diagnostics.
|
||||
|
||||
## Available shared interfaces
|
||||
## Current V2 semantics
|
||||
|
||||
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, and optional spot/futures order management.
|
||||
- Ticker operations are `GetTickerAsync` and `GetAllTickersAsync`, returning `SharedTicker` for both spot and futures.
|
||||
- Socket order streams use `SharedSpotOrderUpdate` and `SharedFuturesOrderUpdate`.
|
||||
- `ICloseFullPosition` closes a complete position, not a partial quantity.
|
||||
- Exchange support varies by operation, transport, trading mode, and request parameter.
|
||||
|
||||
Each exchange library implements a subset. Check exchange docs for support matrix.
|
||||
## Engineering conventions
|
||||
|
||||
## Avoid
|
||||
- Reuse clients through dependency injection.
|
||||
- Use `await`; never use `.Result` or `.Wait()`.
|
||||
- Use `Task.WhenAll` for independent requests across exchanges.
|
||||
- Keep exchange-native models out of portable services.
|
||||
- Do not infer feature support from a broad interface or request model.
|
||||
|
||||
- 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.
|
||||
See `AGENTS.md`, `docs/ai-api-map.md`, and `docs/SHARED_API_V2_MIGRATION.md` for expanded guidance.
|
||||
|
||||
@@ -287,3 +287,4 @@ __pycache__/
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
CryptoExchange.Net/CryptoExchange.Net.xml
|
||||
CryptoExchange.Net.Web3/CryptoExchange.Net.Web3.xml
|
||||
|
||||
@@ -1,40 +1,39 @@
|
||||
---
|
||||
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.
|
||||
description: Use CryptoExchange.Net Shared API V2 abstractions when generating C#/.NET code for multiple cryptocurrency exchanges, including arbitrage, routing, portfolio aggregation, exchange-agnostic bots, comparison tools, and new exchange-library implementations.
|
||||
---
|
||||
|
||||
# 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.
|
||||
CryptoExchange.Net is the base library behind exchange-specific libraries such as Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, and Coinbase.Net. Do not install it alone to call an exchange.
|
||||
|
||||
**Three usage modes:**
|
||||
Choose one of these approaches:
|
||||
|
||||
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.
|
||||
1. One exchange: install and use that exchange's library directly.
|
||||
2. Multiple exchanges: install the required exchange libraries and use `CryptoExchange.Net.SharedApis` V2 capabilities.
|
||||
3. All exchanges in one package: install `CryptoClients.Net` and use its combined clients and shared capability lookup.
|
||||
|
||||
For new cross-exchange code, use Shared API V2. V1 aggregate interfaces remain available through `.SharedClient` for incremental migration.
|
||||
|
||||
## 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:
|
||||
Or install the bundle:
|
||||
|
||||
```bash
|
||||
dotnet add package CryptoClients.Net
|
||||
```
|
||||
|
||||
## Core Pattern: Shared Interfaces
|
||||
## Core pattern: fine-grained capabilities
|
||||
|
||||
Every exchange library exposes `.SharedClient` properties on its API surfaces. These implement the same interfaces from `CryptoExchange.Net.SharedApis`.
|
||||
Each exchange API surface exposes a typed `.SharedApi` aggregate. Assign it to the capability for the single operation being used:
|
||||
|
||||
```csharp
|
||||
using Binance.Net.Clients;
|
||||
@@ -42,155 +41,187 @@ 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;
|
||||
IGetTickerRest binance = new BinanceRestClient().SpotApi.SharedApi;
|
||||
IGetTickerRest okx = new OKXRestClient().UnifiedApi.SharedApi;
|
||||
IGetTickerRest bybit = new BybitRestClient().V5Api.SharedApi;
|
||||
|
||||
// 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
|
||||
```
|
||||
var result = await binance.GetTickerAsync(new GetTickerRequest(symbol));
|
||||
|
||||
## 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.
|
||||
|
||||
## Symbol Metadata and Asset Classification
|
||||
|
||||
Since CryptoExchange.Net 12.2.0, shared symbol responses describe both sides of a market with `BaseAssetType`, `BaseAssetSubType`, `QuoteAssetType`, and `QuoteAssetSubType`. `SharedAssetType` distinguishes `Crypto`, `Fiat`, and `TradFi`; `SharedAssetSubType` distinguishes `StableCoin`, `Equity`, and `Commodity`. `SharedSpotSymbol` and `SharedFuturesSymbol` also expose `DisplayName`.
|
||||
|
||||
The same fields on `GetSymbolsRequest` filter spot or futures symbol discovery:
|
||||
|
||||
```csharp
|
||||
var request = new GetSymbolsRequest(
|
||||
baseAssetType: SharedAssetType.Crypto,
|
||||
quoteAssetSubType: SharedAssetSubType.StableCoin);
|
||||
|
||||
var result = await symbolClient.GetSpotSymbolsAsync(request);
|
||||
```
|
||||
|
||||
After calling `GetSpotSymbolsAsync` or `GetFuturesSymbolsAsync`, use the client's `SpotSymbolCatalog` or `FuturesSymbolCatalog` to look up normalized asset and symbol metadata by name. The catalog is unavailable until the corresponding symbol request has populated the cache.
|
||||
|
||||
For exchange-library implementations, `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` provide best-effort classification of known assets and accept exchange-specific additions. These helpers are heuristics, not an exhaustive source of truth.
|
||||
|
||||
## Shared Market-Data Quantities
|
||||
|
||||
Since CryptoExchange.Net 12.4.0, shared market-data models use `SharedOrderQuantity` so base-asset, quote-asset, and contract quantities remain explicit. Read `SharedSpotTicker.Volumes`, `SharedFuturesTicker.Volumes`, and `SharedKline.Volumes`; read `SharedTrade.Quantities`. The former scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
|
||||
|
||||
## WebSocket Order Management
|
||||
|
||||
Since CryptoExchange.Net 12.5.0, exchanges can implement `ISpotOrderManagementSocketClient` and `IFuturesOrderManagementSocketClient` to place and cancel orders over WebSocket. These are command interfaces, not subscription interfaces: `Place*OrderAsync` and `Cancel*OrderAsync` return `QueryResult<SharedId>`. Check exchange support before using them.
|
||||
|
||||
## 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`
|
||||
- Order commands: `ISpotOrderManagementSocketClient`, `IFuturesOrderManagementSocketClient`
|
||||
|
||||
Each exchange documents which interfaces it implements (some exchanges don't support every operation).
|
||||
|
||||
## Core Pattern: Result Handling
|
||||
|
||||
Same as exchange-specific libraries: REST calls return `HttpResult<T>` and websocket subscription calls return `WebSocketResult<UpdateSubscription>`, both with `.Success`, `.Data`, and `.Error`. Always check `.Success` first.
|
||||
|
||||
```csharp
|
||||
var result = await sharedClient.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||
if (!result.Success)
|
||||
{
|
||||
Console.WriteLine($"[{sharedClient.Exchange}] Error: {result.Error}");
|
||||
return;
|
||||
}
|
||||
Console.WriteLine($"[{sharedClient.Exchange}] {result.Data.LastPrice}");
|
||||
Console.WriteLine($"[{result.Exchange}] {result.Error}");
|
||||
else
|
||||
Console.WriteLine($"[{result.Exchange}] {result.Data!.LastPrice}");
|
||||
```
|
||||
|
||||
`.Exchange` property on every shared client tells you which exchange you're talking to — useful for logging.
|
||||
V2 interfaces describe operations, not broad feature groups. Examples include `IGetTickerRest`, `IGetOrderBookRest`, `IPlaceSpotOrderRest`, `ICancelFuturesOrderRest`, and `ISubscribeTradesSocket`. An exchange may implement one operation without implementing adjacent operations.
|
||||
|
||||
## Core Pattern: Multi-Exchange Aggregation
|
||||
## Shared symbols
|
||||
|
||||
Use `SharedSymbol`; never pass exchange-native symbol strings to shared requests:
|
||||
|
||||
```csharp
|
||||
var clients = new ISpotTickerRestClient[]
|
||||
{
|
||||
new BinanceRestClient().SpotApi.SharedClient,
|
||||
new OKXRestClient().UnifiedApi.SharedClient,
|
||||
new BybitRestClient().V5Api.SharedClient,
|
||||
};
|
||||
var spot = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
var linearPerpetual = new SharedSymbol(
|
||||
TradingMode.PerpetualLinear,
|
||||
"BTC",
|
||||
"USDT");
|
||||
```
|
||||
|
||||
Each exchange library converts the shared symbol to its native format. For unusual exchange asset names, configure asset aliases.
|
||||
|
||||
## Known surface versus runtime selection
|
||||
|
||||
Use the typed `.SharedApi` surface when the exchange and API are known. This is the simplest and most discoverable approach:
|
||||
|
||||
```csharp
|
||||
IPlaceSpotOrderRest orders = restClient.SpotApi.SharedApi;
|
||||
```
|
||||
|
||||
For runtime selection, inject the exchange-wide `I[Exchange]SharedApiClient` registered by `services.Add[Exchange](...)`. Resolve a capability with the strongly typed `SharedCapabilities` catalog:
|
||||
|
||||
```csharp
|
||||
var match = sharedClient.GetCapability(
|
||||
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
|
||||
TradingMode.PerpetualLinear);
|
||||
|
||||
if (match is null)
|
||||
return; // This exchange/API/mode does not provide the operation.
|
||||
|
||||
var result = await match.Capability.PlaceFuturesOrderAsync(request);
|
||||
```
|
||||
|
||||
`GetCapability` returns one `SharedCapabilityResolution<T>` containing `Capability` and `Options`, or `null` if no supported implementation matches. `GetCapabilities` returns every match on that exchange-wide client. Always specify `TradingMode` when multiple spot, linear, inverse, or delivery surfaces could match.
|
||||
|
||||
`SharedCapabilities` references identify interface types; they do not guarantee that an exchange implements the operation and do not send a request.
|
||||
|
||||
## Transport selection and result types
|
||||
|
||||
Some commands have three interfaces:
|
||||
|
||||
- Transport-agnostic, such as `IPlaceSpotOrder`, returns `IExchangeCallResult<T>`.
|
||||
- REST-specific, such as `IPlaceSpotOrderRest`, returns `HttpResult<T>`.
|
||||
- socket-specific, such as `IPlaceSpotOrderSocket`, returns `QueryResult<T>`.
|
||||
|
||||
Socket subscription capabilities such as `ISubscribeTickerSocket` return `WebSocketResult<UpdateSubscription>`.
|
||||
|
||||
Use `.Rest` or `.Socket` on a capability reference when transport matters. Without a transport filter, exchange-wide lookup uses `PreferredTransport`, configured through the exchange's `SharedApi.PreferredTransport` option and normally defaulting to REST.
|
||||
|
||||
Always check `.Success` before `.Data`. Use `.Error` for failures and the result or capability `.Exchange` value for multi-exchange logging.
|
||||
|
||||
## Inspect parameter support
|
||||
|
||||
The presence of a capability does not mean every shared request property is accepted by every exchange. Inspect its `CapabilityOptions`:
|
||||
|
||||
- `RequestParameterRules`: whether each shared request field is `Required`, `Optional`, or `NotSupported`.
|
||||
- `ExchangeParameterRules`: required or optional exchange-specific values supplied through the request's `ExchangeParameters`.
|
||||
- `SupportedTradingModes`: the trading modes supported by this implementation.
|
||||
|
||||
```csharp
|
||||
var match = sharedClient.GetCapability(
|
||||
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
|
||||
TradingMode.PerpetualLinear);
|
||||
|
||||
var leverageRule = match?.Options.RequestParameterRules
|
||||
.FirstOrDefault(x => x.Name == nameof(PlaceFuturesOrderRequest.Leverage));
|
||||
```
|
||||
|
||||
Do not infer support from the request model alone. Handle a `null` resolution and exchange-specific parameter rules before issuing dynamic trading calls.
|
||||
|
||||
## Common capabilities
|
||||
|
||||
- Market data: `IGetTickerRest`, `IGetAllTickersRest`, `IGetOrderBookRest`, `IGetKlinesRest`, `IGetRecentTradesRest`; ticker, trade, kline, and order-book socket subscriptions.
|
||||
- Trading: fine-grained spot and futures place, edit, cancel, get, open-order, closed-order, and order-update capabilities.
|
||||
- Account: balances, positions, user trades, fees, deposits, withdrawals, and transfers.
|
||||
- Futures data: funding, open interest, leverage, mark price, and index price capabilities.
|
||||
|
||||
Check the typed exchange Shared API surface or use capability lookup instead of assuming universal support.
|
||||
|
||||
## Multi-exchange aggregation
|
||||
|
||||
Run independent exchange requests concurrently:
|
||||
|
||||
```csharp
|
||||
var clients = new IGetTickerRest[] { binance, okx, bybit };
|
||||
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 tasks = clients.Select(client =>
|
||||
client.GetTickerAsync(new GetTickerRequest(symbol)));
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
for (int i = 0; i < clients.Length; i++)
|
||||
foreach (var result in results.Where(x => x.Success))
|
||||
Console.WriteLine($"{result.Exchange}: {result.Data!.LastPrice}");
|
||||
```
|
||||
|
||||
Reuse clients through dependency injection; do not instantiate them per request.
|
||||
|
||||
## Dependency injection
|
||||
|
||||
Each exchange library provides `services.Add[Exchange](...)`. Registration includes its native clients, V1 shared interfaces, V2 operation capabilities, and exchange-wide shared client.
|
||||
|
||||
Inject a capability directly only when the container has one intended implementation:
|
||||
|
||||
```csharp
|
||||
public sealed class TickerService(IGetTickerRest ticker)
|
||||
{
|
||||
if (results[i].Success)
|
||||
Console.WriteLine($"{clients[i].Exchange}: {results[i].Data!.LastPrice}");
|
||||
public Task<HttpResult<SharedTicker>> GetAsync(
|
||||
SharedSymbol symbol,
|
||||
CancellationToken ct = default)
|
||||
=> ticker.GetTickerAsync(new GetTickerRequest(symbol), ct);
|
||||
}
|
||||
```
|
||||
|
||||
## Per-Exchange Setup
|
||||
If multiple exchanges or multiple API surfaces register the same capability, inject the exchange-specific shared client and select its typed API property or call `GetCapability`. Plain single-service resolution does not express which implementation you want.
|
||||
|
||||
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(...); })`.
|
||||
For the bundle, use `services.AddCryptoClients(...)`. CryptoClients.Net provides `IExchangeSharedApiClient` for capability lookup across exchanges.
|
||||
|
||||
## Dependency Injection
|
||||
## V1 migration
|
||||
|
||||
Each exchange library has its own `services.AddXxx(...)` extension. They all share the same option-builder pattern. Register only the ones you use:
|
||||
V1 `.SharedClient` facades and broad interfaces such as `ISpotTickerRestClient` remain available. Migrate one operation at a time:
|
||||
|
||||
```csharp
|
||||
services.AddBinance(restOpts => { /*...*/ }, socketOpts => { /*...*/ });
|
||||
services.AddOKX(restOpts => { /*...*/ }, socketOpts => { /*...*/ });
|
||||
// Inject IBinanceRestClient, IOKXRestClient, etc.
|
||||
// V1
|
||||
await restClient.SpotApi.SharedClient.GetSpotTickerAsync(request);
|
||||
|
||||
// V2
|
||||
await restClient.SpotApi.SharedApi.GetTickerAsync(request);
|
||||
```
|
||||
|
||||
For one-package access: `services.AddCryptoClients(...)` from `CryptoClients.Net`.
|
||||
Important semantic changes:
|
||||
|
||||
## Common Pitfalls — AVOID
|
||||
- V2 ticker methods are `GetTickerAsync` and `GetAllTickersAsync` and return `SharedTicker` for both spot and futures.
|
||||
- WebSocket order updates use `SharedSpotOrderUpdate` and `SharedFuturesOrderUpdate`; REST order retrieval keeps the ordinary order models.
|
||||
- `ICloseFullPosition` closes the complete position. Use an order capability for partial closes where supported.
|
||||
- Transport-agnostic operations return `IExchangeCallResult<T>`; select the REST or socket interface when code needs a transport-specific result.
|
||||
|
||||
- **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.
|
||||
See `docs/SHARED_API_V2_MIGRATION.md` for detailed mappings.
|
||||
|
||||
## Implementing a New Exchange Library
|
||||
## Common pitfalls
|
||||
|
||||
If you're building a NEW exchange wrapper following the CryptoExchange.Net pattern (rare but valuable):
|
||||
- Do not install `CryptoExchange.Net` alone and expect exchange endpoints.
|
||||
- Do not use `.SharedClient` for new V2 code; use `.SharedApi` and fine-grained capabilities.
|
||||
- Do not mix exchange-native request or response models into cross-exchange services.
|
||||
- Do not assume capability presence or request-parameter support; resolve and inspect it.
|
||||
- Do not rely on preferred transport when REST or socket semantics matter.
|
||||
- Do not block with `.Result` or `.Wait()`; use async calls throughout.
|
||||
- Do not query exchanges sequentially when requests are independent.
|
||||
|
||||
- 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
|
||||
## Implementing a new exchange library
|
||||
|
||||
See existing libraries (Binance.Net, Bybit.Net) as reference implementations.
|
||||
- Derive API clients from `RestApiClient` and `SocketApiClient`.
|
||||
- Follow the exchange credentials and options patterns used by existing libraries.
|
||||
- Implement the relevant fine-grained V2 capability interfaces on typed Shared API classes.
|
||||
- Publish accurate `CapabilityOptions`, including supported trading modes and request/exchange parameter rules.
|
||||
- Register REST/socket Shared APIs and the exchange-wide shared client with the library's DI extension.
|
||||
- Retain V1 facades only where compatibility with existing consumers is required.
|
||||
|
||||
Use maintained exchange libraries such as Binance.Net and Bybit.Net as implementation references.
|
||||
|
||||
## 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
|
||||
- Shared API migration: https://github.com/JKorf/CryptoExchange.Net/blob/master/docs/SHARED_API_V2_MIGRATION.md
|
||||
- Bundle: https://github.com/JKorf/CryptoClients.Net
|
||||
- Demo app: https://github.com/JKorf/CryptoManager.Net
|
||||
- Discord: https://discord.gg/MSpeEtSY8t
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Web3
|
||||
{
|
||||
/// <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>
|
||||
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>
|
||||
public static byte[] AbiValueEncodeInt(uint value)
|
||||
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode ulong value as uint256, as per ABI specification
|
||||
/// </summary>
|
||||
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,
|
||||
int bitWidth = 256)
|
||||
{
|
||||
if (bitWidth < 8 || bitWidth > 256 || bitWidth % 8 != 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(bitWidth));
|
||||
|
||||
var min = signed
|
||||
? -(BigInteger.One << (bitWidth - 1))
|
||||
: BigInteger.Zero;
|
||||
|
||||
var max = signed
|
||||
? (BigInteger.One << (bitWidth - 1)) - 1
|
||||
: (BigInteger.One << bitWidth) - 1;
|
||||
|
||||
if (value < min || value > max)
|
||||
throw new ArgumentOutOfRangeException(nameof(value), $"Value does not fit {(signed ? "int" : "uint")}{bitWidth}.");
|
||||
|
||||
var result = new byte[32];
|
||||
|
||||
// Sign-extend negative values.
|
||||
if (value.Sign < 0)
|
||||
{
|
||||
for (var i = 0; i < result.Length; i++)
|
||||
result[i] = 0xff;
|
||||
}
|
||||
|
||||
// BigInteger.ToByteArray() always returns little-endian
|
||||
// two's-complement bytes, regardless of platform endianness.
|
||||
var bytes = value.ToByteArray();
|
||||
|
||||
// A valid uint256 may have a 33rd, zero-valued sign byte.
|
||||
for (var i = 0; i < Math.Min(bytes.Length, result.Length); i++)
|
||||
result[result.Length - 1 - i] = bytes[i];
|
||||
|
||||
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>
|
||||
public static byte[] AbiValueEncodeHexBytes(int length, string value)
|
||||
=> AbiValueEncodeBytes(length, value.HexStringToBytes());
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode byte array value as bytes32, as per ABI specification. <br />
|
||||
/// The resulting bytes will be left aligned in the 32 bytes result, with trailing zeros if the byte array is shorter than 32 bytes. <br />
|
||||
/// If the byte array is longer than 32 bytes, an exception will be thrown.
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeBytes(int length, byte[] value)
|
||||
{
|
||||
if (length < 1 || length > 32)
|
||||
throw new ArgumentOutOfRangeException(nameof(length));
|
||||
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
|
||||
if (value.Length > length)
|
||||
throw new ArgumentException($"Value exceeds bytes{length}.", nameof(value));
|
||||
|
||||
// Fixed bytes are left-aligned, with trailing zero padding.
|
||||
var result = new byte[32];
|
||||
Array.Copy(value, 0, result, 0, value.Length);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
using System.Collections;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace CryptoExchange.Net.Web3
|
||||
{
|
||||
/// <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[] EncodeEip712(
|
||||
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", StringComparison.Ordinal) ||
|
||||
memberValue.TypeName.StartsWith("uint", StringComparison.Ordinal))
|
||||
{
|
||||
var signed = memberValue.TypeName[0] != 'u';
|
||||
var widthText = memberValue.TypeName.Substring(signed ? 3 : 4);
|
||||
|
||||
if (!int.TryParse( widthText, NumberStyles.None, CultureInfo.InvariantCulture, out var bitWidth))
|
||||
throw new ArgumentException($"Invalid integer type: {memberValue.TypeName}");
|
||||
|
||||
var value = memberValue.Value switch
|
||||
{
|
||||
BigInteger n => n,
|
||||
byte n => new BigInteger(n),
|
||||
sbyte n => new BigInteger(n),
|
||||
short n => new BigInteger(n),
|
||||
ushort n => new BigInteger(n),
|
||||
int n => new BigInteger(n),
|
||||
uint n => new BigInteger(n),
|
||||
long n => new BigInteger(n),
|
||||
ulong n => new BigInteger(n),
|
||||
|
||||
string text => BigInteger.Parse(text, NumberStyles.Integer, CultureInfo.InvariantCulture),
|
||||
|
||||
_ => throw new ArgumentException($"Unsupported value type for {memberValue.TypeName}")
|
||||
};
|
||||
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeBigInteger(signed, value, bitWidth));
|
||||
}
|
||||
else if (memberValue.TypeName.StartsWith("bytes", StringComparison.Ordinal))
|
||||
{
|
||||
if (memberValue.Value is not byte[] value)
|
||||
throw new ArgumentException($"Value for {memberValue.TypeName} must be byte[].");
|
||||
|
||||
if (memberValue.TypeName == "bytes")
|
||||
{
|
||||
// EIP-712 dynamic bytes: hash the entire contents,
|
||||
// including empty arrays, without padding first.
|
||||
writer.Write(CeSha3Keccack.CalculateHash(value));
|
||||
}
|
||||
else
|
||||
{
|
||||
var lengthText = memberValue.TypeName.Substring(5);
|
||||
if (!int.TryParse(lengthText, NumberStyles.None, CultureInfo.InvariantCulture, out var length))
|
||||
throw new ArgumentException($"Invalid bytes type: {memberValue.TypeName}");
|
||||
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeBytes(length, value));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"Unsupported EIP-712 type: {memberValue.TypeName}");
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EncodeType(BinaryWriter writer, IDictionary<string, CeMemberDescription[]> types, string typeName)
|
||||
{
|
||||
var encodedTypes = EncodeTypes(types, typeName);
|
||||
var encodedPrimaryType = encodedTypes.SingleOrDefault(x => x.Key == typeName);
|
||||
if (encodedPrimaryType.Equals(default(KeyValuePair<string, string>)))
|
||||
throw new Exception($"Type {typeName} not found in types dictionary");
|
||||
|
||||
var fullyEncodedType = string.Concat(encodedTypes.Select(x => x.Value));
|
||||
writer.Write(CeSha3Keccack.CalculateHash(Encoding.UTF8.GetBytes(fullyEncodedType)));
|
||||
}
|
||||
|
||||
private static IList<KeyValuePair<string, string>> EncodeTypes(
|
||||
IDictionary<string, CeMemberDescription[]> types,
|
||||
string currentTypeName)
|
||||
{
|
||||
var visited = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
void Visit(string typeName)
|
||||
{
|
||||
// Prevent duplicate dependencies and recursive traversal loops.
|
||||
if (!visited.Add(typeName))
|
||||
return;
|
||||
|
||||
if (!types.TryGetValue(typeName, out var members))
|
||||
throw new ArgumentException($"Type {typeName} not found in types dictionary", nameof(types));
|
||||
|
||||
foreach (var member in members)
|
||||
{
|
||||
var memberType = member.Type;
|
||||
|
||||
// Strip all array dimensions to find the underlying type.
|
||||
var arrayIndex = memberType.IndexOf('[');
|
||||
if (arrayIndex >= 0)
|
||||
memberType = memberType.Substring(0, arrayIndex);
|
||||
|
||||
if (IsReferenceType(memberType))
|
||||
Visit(memberType);
|
||||
}
|
||||
}
|
||||
|
||||
KeyValuePair<string, string> Encode(string typeName)
|
||||
{
|
||||
var members = types[typeName]
|
||||
.Select(x => $"{x.Type} {x.Name}");
|
||||
|
||||
return new KeyValuePair<string, string>(
|
||||
typeName,
|
||||
$"{typeName}({string.Join(",", members)})");
|
||||
}
|
||||
|
||||
Visit(currentTypeName);
|
||||
|
||||
var result = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
Encode(currentTypeName)
|
||||
};
|
||||
|
||||
result.AddRange(
|
||||
visited
|
||||
.Where(x => !StringComparer.Ordinal.Equals(x, currentTypeName))
|
||||
.OrderBy(x => x, StringComparer.Ordinal)
|
||||
.Select(Encode));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static bool IsReferenceType(string typeName)
|
||||
{
|
||||
if (typeName == "bytes" ||
|
||||
typeName == "string" ||
|
||||
typeName == "bool" ||
|
||||
typeName == "address" ||
|
||||
typeName.Contains("["))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var isPrimitive = Regex.IsMatch(
|
||||
typeName,
|
||||
@"\A(?:bytes[0-9]+|u?int[0-9]+)\z");
|
||||
|
||||
return !isPrimitive;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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; } = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.Web3
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PackageId>CryptoExchange.Net.Web3</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>CryptoExchange.Net.Web3 is a helper library adding Web3 utilities to CryptoExchange.Net implementations.</Description>
|
||||
<PackageVersion>13.0.0</PackageVersion>
|
||||
<AssemblyVersion>13.0.0</AssemblyVersion>
|
||||
<FileVersion>13.0.0</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\CryptoExchange.Net\Icon\icon.png" Pack="true" PackagePath="\" />
|
||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="AOT" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<DocumentationFile>CryptoExchange.Net.Web3.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CryptoExchange.Net" Version="13.2.2" />
|
||||
<PackageReference Include="Secp256k1.Net" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,171 @@
|
||||
using Secp256k1Net;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Web3
|
||||
{
|
||||
/// <summary>
|
||||
/// Eth public/private key pair, used for signing messages and verifying signatures.
|
||||
/// </summary>
|
||||
public class EthKey
|
||||
{
|
||||
private byte[]? _pkBytes;
|
||||
|
||||
/// <summary>
|
||||
/// Private key
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string? PrivateKey { get; }
|
||||
/// <summary>
|
||||
/// Public address
|
||||
/// </summary>
|
||||
public string PublicAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new EthKey instance
|
||||
/// </summary>
|
||||
/// <param name="publicAddress">Public address, can not sign messages without private key</param>
|
||||
/// <param name="privateKey">Private key, required for signing messages</param>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public EthKey(string? publicAddress = null, string? privateKey = null)
|
||||
{
|
||||
if (privateKey == null && publicAddress == null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Either a private key or public address must be provided.");
|
||||
}
|
||||
|
||||
if (privateKey != null)
|
||||
{
|
||||
if (!privateKey.StartsWith("0x") || privateKey.Length != 66)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Private key must be a valid ETH private key, starting with 0x and 66 characters long.");
|
||||
}
|
||||
|
||||
PrivateKey = privateKey;
|
||||
_pkBytes = HexToBytesString(privateKey);
|
||||
}
|
||||
|
||||
if (publicAddress != null)
|
||||
{
|
||||
if (!publicAddress.StartsWith("0x") || publicAddress.Length != 42)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Public address must be a valid ETH address, starting with 0x and 42 characters long.");
|
||||
}
|
||||
|
||||
if (privateKey != null && !publicAddress.Equals(GetPublicAddress(privateKey), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Private key does not match the public address.");
|
||||
}
|
||||
|
||||
PublicAddress = publicAddress;
|
||||
}
|
||||
else
|
||||
{
|
||||
PublicAddress = GetPublicAddress(privateKey!);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create new Eth credentials with only a public key. This is generally only sufficient for verifying signatures and read operations. <br />
|
||||
/// Request signing will not be possible without a private key.
|
||||
/// </summary>
|
||||
/// <param name="publicAddress">The public wallet address</param>
|
||||
public static EthKey FromPublicAddress(string publicAddress)
|
||||
{
|
||||
return new EthKey(publicAddress, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create new Eth credentials from a private key. The public key will be derived from the private key. Allows signing of messages.
|
||||
/// </summary>
|
||||
/// <param name="privateKey">The private key for the wallet</param>
|
||||
public static EthKey FromPrivateKey(string privateKey)
|
||||
{
|
||||
return new EthKey(null, privateKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign a hash with the private key, returning the signature in rsv format
|
||||
/// </summary>
|
||||
public Dictionary<string, object> SignHashRsv(byte[] request)
|
||||
{
|
||||
if (_pkBytes == null)
|
||||
throw new InvalidOperationException("Private key not provided, cannot sign hash");
|
||||
|
||||
(var signature, var recover) = Secp256k1.SignRecoverable(request, _pkBytes);
|
||||
var hexCompactR = BytesToHexString(new ArraySegment<byte>(signature, 0, 32));
|
||||
var hexCompactS = BytesToHexString(new ArraySegment<byte>(signature, 32, 32));
|
||||
var hexCompactV = recover + 27;
|
||||
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "r", "0x" + hexCompactR },
|
||||
{ "s", "0x" + hexCompactS },
|
||||
{ "v", hexCompactV },
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign a hash with the private key, returning the signature in signature format
|
||||
/// </summary>
|
||||
public string SignHash(byte[] request)
|
||||
{
|
||||
if (_pkBytes == null)
|
||||
throw new InvalidOperationException("Private key not provided, cannot sign hash");
|
||||
|
||||
(var signature, var recover) = Secp256k1.SignRecoverable(request, _pkBytes);
|
||||
var hexCompactR = BytesToHexString(new ArraySegment<byte>(signature, 0, 32));
|
||||
var hexCompactS = BytesToHexString(new ArraySegment<byte>(signature, 32, 32));
|
||||
var hexCompactV = BytesToHexString([(byte)(recover + 27)]);
|
||||
|
||||
var result = "0x" + hexCompactR.PadLeft(64, '0') +
|
||||
hexCompactS.PadLeft(64, '0') +
|
||||
hexCompactV;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => $"EthKey(PublicAddress={PublicAddress})";
|
||||
|
||||
private static string GetPublicAddress(string privateKey)
|
||||
{
|
||||
var publicKeyBytes = Secp256k1.CreatePublicKey(ExchangeHelpers.HexToBytesString(privateKey), false);
|
||||
|
||||
var withoutPrefix = new byte[64];
|
||||
Array.Copy(publicKeyBytes, 1, withoutPrefix, 0, 64);
|
||||
|
||||
var hash = CeSha3Keccack.CalculateHash(withoutPrefix);
|
||||
var pubAddress = new byte[20];
|
||||
Array.Copy(hash, hash.Length - 20, pubAddress, 0, 20);
|
||||
|
||||
return "0x" + ExchangeHelpers.BytesToHexString(pubAddress).ToLower();
|
||||
}
|
||||
|
||||
|
||||
private static byte[] HexToBytesString(string hexString)
|
||||
{
|
||||
if (hexString.StartsWith("0x"))
|
||||
hexString = hexString.Substring(2);
|
||||
|
||||
byte[] array = new byte[hexString.Length / 2];
|
||||
for (int i = 0; i < hexString.Length; i += 2)
|
||||
{
|
||||
string value = hexString.Substring(i, 2);
|
||||
array[i / 2] = Convert.ToByte(value, 16);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
private static string BytesToHexString(IEnumerable<byte> buff)
|
||||
{
|
||||
string text = string.Empty;
|
||||
foreach (byte item in buff)
|
||||
text += item.ToString("X2");
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
using System.Collections;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Web3
|
||||
{
|
||||
/// <summary>
|
||||
/// Message pack converter, not thread safe
|
||||
/// </summary>
|
||||
public class MessagePackConverter
|
||||
{
|
||||
private byte[] _tmp0 = new byte[8];
|
||||
private Encoding _encoder = Encoding.UTF8;
|
||||
|
||||
/// <summary>
|
||||
/// Convert an object to byte array
|
||||
/// </summary>
|
||||
public byte[] Pack(object o)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
Pack(ms, o);
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert an object to byte array
|
||||
/// </summary>
|
||||
public void Pack(Stream s, object o)
|
||||
{
|
||||
if (o == null)
|
||||
PackNull(s);
|
||||
else if (o is string objStr)
|
||||
Pack(s, objStr);
|
||||
else if (o is IList objList)
|
||||
Pack(s, objList);
|
||||
else if (o is IDictionary objDict)
|
||||
Pack(s, objDict);
|
||||
else if (o is IDictionary<string, object> objDict2)
|
||||
Pack(s, objDict2);
|
||||
else if (o is bool objBool)
|
||||
Pack(s, objBool);
|
||||
else if (o is sbyte objSbyte)
|
||||
Pack(s, objSbyte);
|
||||
else if (o is byte objByte)
|
||||
Pack(s, objByte);
|
||||
else if (o is short objShort)
|
||||
Pack(s, objShort);
|
||||
else if (o is ushort objUshort)
|
||||
Pack(s, objUshort);
|
||||
else if (o is int objInt)
|
||||
Pack(s, objInt);
|
||||
else if (o is uint objUint)
|
||||
Pack(s, objUint);
|
||||
else if (o is long objLong)
|
||||
Pack(s, objLong);
|
||||
else if (o is ulong objUlong)
|
||||
Pack(s, objUlong);
|
||||
else if (o is float objFloat)
|
||||
Pack(s, objFloat);
|
||||
else if (o is double objDouble)
|
||||
Pack(s, objDouble);
|
||||
else
|
||||
Pack(s, Convert.ToString(o, CultureInfo.InvariantCulture)!);
|
||||
}
|
||||
|
||||
private void PackNull(Stream s)
|
||||
{
|
||||
s.WriteByte(0xc0);
|
||||
}
|
||||
|
||||
private void Pack(Stream s, IList list)
|
||||
{
|
||||
int count = list.Count;
|
||||
if (count < 16)
|
||||
{
|
||||
s.WriteByte((byte)(0x90 + count));
|
||||
}
|
||||
else if (count < 0x10000)
|
||||
{
|
||||
s.WriteByte(0xdc);
|
||||
Write(s, (ushort)count);
|
||||
}
|
||||
else
|
||||
{
|
||||
s.WriteByte(0xdd);
|
||||
Write(s, (uint)count);
|
||||
}
|
||||
foreach (object o in list)
|
||||
{
|
||||
Pack(s, o);
|
||||
}
|
||||
}
|
||||
|
||||
private void Pack(Stream s, IDictionary dict)
|
||||
{
|
||||
int count = dict.Count;
|
||||
if (count < 16)
|
||||
{
|
||||
s.WriteByte((byte)(0x80 + count));
|
||||
}
|
||||
else if (count < 0x10000)
|
||||
{
|
||||
s.WriteByte(0xde);
|
||||
Write(s, (ushort)count);
|
||||
}
|
||||
else
|
||||
{
|
||||
s.WriteByte(0xdf);
|
||||
Write(s, (uint)count);
|
||||
}
|
||||
foreach (object key in dict.Keys)
|
||||
{
|
||||
Pack(s, key);
|
||||
Pack(s, dict[key]!);
|
||||
}
|
||||
}
|
||||
|
||||
private void Pack(Stream s, IDictionary<string, object> dict)
|
||||
{
|
||||
int count = dict.Count;
|
||||
if (count < 16)
|
||||
{
|
||||
s.WriteByte((byte)(0x80 + count));
|
||||
}
|
||||
else if (count < 0x10000)
|
||||
{
|
||||
s.WriteByte(0xde);
|
||||
Write(s, (ushort)count);
|
||||
}
|
||||
else
|
||||
{
|
||||
s.WriteByte(0xdf);
|
||||
Write(s, (uint)count);
|
||||
}
|
||||
foreach (var key in dict.Keys)
|
||||
{
|
||||
Pack(s, key);
|
||||
Pack(s, dict[key]!);
|
||||
}
|
||||
}
|
||||
|
||||
private void Pack(Stream s, bool val)
|
||||
{
|
||||
s.WriteByte(val ? (byte)0xc3 : (byte)0xc2);
|
||||
}
|
||||
|
||||
private void Pack(Stream s, sbyte val)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
if (val >= -32)
|
||||
{
|
||||
s.WriteByte((byte)val);
|
||||
}
|
||||
else
|
||||
{
|
||||
_tmp0[0] = 0xd0;
|
||||
_tmp0[1] = (byte)val;
|
||||
s.Write(_tmp0, 0, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Pack(Stream s, byte val)
|
||||
{
|
||||
if (val <= 0x7f)
|
||||
{
|
||||
s.WriteByte(val);
|
||||
}
|
||||
else
|
||||
{
|
||||
_tmp0[0] = 0xcc;
|
||||
_tmp0[1] = val;
|
||||
s.Write(_tmp0, 0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
private void Pack(Stream s, short val)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
if (val >= 0)
|
||||
{
|
||||
Pack(s, (ushort)val);
|
||||
}
|
||||
else if (val >= -128)
|
||||
{
|
||||
Pack(s, (sbyte)val);
|
||||
}
|
||||
else
|
||||
{
|
||||
s.WriteByte(0xd1);
|
||||
Write(s, (ushort)val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Pack(Stream s, ushort val)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
if (val < 0x100)
|
||||
{
|
||||
Pack(s, (byte)val);
|
||||
}
|
||||
else
|
||||
{
|
||||
s.WriteByte(0xcd);
|
||||
Write(s, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Pack(Stream s, int val)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
if (val >= 0)
|
||||
{
|
||||
Pack(s, (uint)val);
|
||||
}
|
||||
else if (val >= -128)
|
||||
{
|
||||
Pack(s, (sbyte)val);
|
||||
}
|
||||
else if (val >= -0x8000)
|
||||
{
|
||||
s.WriteByte(0xd1);
|
||||
Write(s, (ushort)val);
|
||||
}
|
||||
else
|
||||
{
|
||||
s.WriteByte(0xd2);
|
||||
Write(s, (uint)val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Pack(Stream s, uint val)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
if (val < 0x100)
|
||||
{
|
||||
Pack(s, (byte)val);
|
||||
}
|
||||
else if (val < 0x10000)
|
||||
{
|
||||
s.WriteByte(0xcd);
|
||||
Write(s, (ushort)val);
|
||||
}
|
||||
else
|
||||
{
|
||||
s.WriteByte(0xce);
|
||||
Write(s, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Pack(Stream s, long val)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
if (val >= 0)
|
||||
{
|
||||
Pack(s, (ulong)val);
|
||||
}
|
||||
else if (val >= -128)
|
||||
{
|
||||
Pack(s, (sbyte)val);
|
||||
}
|
||||
else if (val >= -0x8000)
|
||||
{
|
||||
s.WriteByte(0xd1);
|
||||
Write(s, (ushort)val);
|
||||
}
|
||||
else if (val >= -0x80000000)
|
||||
{
|
||||
s.WriteByte(0xd2);
|
||||
Write(s, (uint)val);
|
||||
}
|
||||
else
|
||||
{
|
||||
s.WriteByte(0xd3);
|
||||
Write(s, (ulong)val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Pack(Stream s, ulong val)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
if (val < 0x100)
|
||||
{
|
||||
Pack(s, (byte)val);
|
||||
}
|
||||
else if (val < 0x10000)
|
||||
{
|
||||
s.WriteByte(0xcd);
|
||||
Write(s, (ushort)val);
|
||||
}
|
||||
else if (val < 0x100000000)
|
||||
{
|
||||
s.WriteByte(0xce);
|
||||
Write(s, (uint)val);
|
||||
}
|
||||
else
|
||||
{
|
||||
s.WriteByte(0xcf);
|
||||
Write(s, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Pack(Stream s, float val)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes(val);
|
||||
s.WriteByte(0xca);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
_tmp0[0] = bytes[3];
|
||||
_tmp0[1] = bytes[2];
|
||||
_tmp0[2] = bytes[1];
|
||||
_tmp0[3] = bytes[0];
|
||||
s.Write(_tmp0, 0, 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
s.Write(bytes, 0, 4);
|
||||
}
|
||||
}
|
||||
|
||||
private void Pack(Stream s, double val)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes(val);
|
||||
s.WriteByte(0xcb);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
_tmp0[0] = bytes[7];
|
||||
_tmp0[1] = bytes[6];
|
||||
_tmp0[2] = bytes[5];
|
||||
_tmp0[3] = bytes[4];
|
||||
_tmp0[4] = bytes[3];
|
||||
_tmp0[5] = bytes[2];
|
||||
_tmp0[6] = bytes[1];
|
||||
_tmp0[7] = bytes[0];
|
||||
s.Write(_tmp0, 0, 8);
|
||||
}
|
||||
else
|
||||
{
|
||||
s.Write(bytes, 0, 8);
|
||||
}
|
||||
}
|
||||
|
||||
private void Pack(Stream s, string val)
|
||||
{
|
||||
var bytes = _encoder.GetBytes(val);
|
||||
if (bytes.Length < 0x20)
|
||||
{
|
||||
s.WriteByte((byte)(0xa0 + bytes.Length));
|
||||
}
|
||||
else if (bytes.Length < 0x100)
|
||||
{
|
||||
s.WriteByte(0xd9);
|
||||
s.WriteByte((byte)bytes.Length);
|
||||
}
|
||||
else if (bytes.Length < 0x10000)
|
||||
{
|
||||
s.WriteByte(0xda);
|
||||
Write(s, (ushort)bytes.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
s.WriteByte(0xdb);
|
||||
Write(s, (uint)bytes.Length);
|
||||
}
|
||||
s.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
private void Write(Stream s, ushort val)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
_tmp0[0] = (byte)(val >> 8);
|
||||
_tmp0[1] = (byte)val;
|
||||
s.Write(_tmp0, 0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
private void Write(Stream s, uint val)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
_tmp0[0] = (byte)(val >> 24);
|
||||
_tmp0[1] = (byte)(val >> 16);
|
||||
_tmp0[2] = (byte)(val >> 8);
|
||||
_tmp0[3] = (byte)val;
|
||||
s.Write(_tmp0, 0, 4);
|
||||
}
|
||||
}
|
||||
|
||||
private void Write(Stream s, ulong val)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
_tmp0[0] = (byte)(val >> 56);
|
||||
_tmp0[1] = (byte)(val >> 48);
|
||||
_tmp0[2] = (byte)(val >> 40);
|
||||
_tmp0[3] = (byte)(val >> 32);
|
||||
_tmp0[4] = (byte)(val >> 24);
|
||||
_tmp0[5] = (byte)(val >> 16);
|
||||
_tmp0[6] = (byte)(val >> 8);
|
||||
_tmp0[7] = (byte)val;
|
||||
s.Write(_tmp0, 0, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#  CryptoExchange.Net.Web3
|
||||
|
||||
Web3 utilities for CryptoExchange.Net implementations.
|
||||
|
||||
## Release notes
|
||||
* Version 13.0.0 - 03 Nov 2026
|
||||
* Initial release
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.32014.148
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.10.12201.205 stable
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoExchange.Net", "CryptoExchange.Net\CryptoExchange.Net.csproj", "{3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}"
|
||||
EndProject
|
||||
@@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\C
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptoExchange.Net.Web3", "CryptoExchange.Net.Web3\CryptoExchange.Net.Web3.csproj", "{7ABCD210-C708-42ED-AF81-85C0A6089A1E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -41,6 +43,10 @@ Global
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7ABCD210-C708-42ED-AF81-85C0A6089A1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7ABCD210-C708-42ED-AF81-85C0A6089A1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7ABCD210-C708-42ED-AF81-85C0A6089A1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7ABCD210-C708-42ED-AF81-85C0A6089A1E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -286,7 +286,7 @@ namespace CryptoExchange.Net.Clients
|
||||
while (true)
|
||||
{
|
||||
// Get a new or existing socket connection
|
||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
|
||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
|
||||
if (!socketResult.Success)
|
||||
return WebSocketResult.Fail<UpdateSubscription>(Exchange, socketResult.Error);
|
||||
|
||||
@@ -309,7 +309,7 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
var needsConnecting = !socketConnection.Connected;
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false);
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct, subscription).ConfigureAwait(false);
|
||||
if (!connectResult.Success)
|
||||
return WebSocketResult.Fail<UpdateSubscription>(Exchange, connectResult.Error!);
|
||||
|
||||
@@ -482,7 +482,7 @@ namespace CryptoExchange.Net.Clients
|
||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
|
||||
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct, processor: query).ConfigureAwait(false);
|
||||
if (!socketResult.Success)
|
||||
return QueryResult.Fail<THandlerResponse>(Exchange, socketResult.Error);
|
||||
|
||||
@@ -495,7 +495,7 @@ namespace CryptoExchange.Net.Clients
|
||||
released = true;
|
||||
}
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false);
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct, query).ConfigureAwait(false);
|
||||
if (!connectResult.Success)
|
||||
return QueryResult.Fail<THandlerResponse>(Exchange, connectResult.Error!);
|
||||
}
|
||||
@@ -524,7 +524,18 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="authenticated">Whether the socket should authenticated</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct)
|
||||
protected virtual Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct)
|
||||
=> ConnectIfNeededAsync(socket, authenticated, ct, null);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a socket needs to be connected and does so if needed. Also authenticates on the socket if needed
|
||||
/// </summary>
|
||||
/// <param name="socket">The connection to check</param>
|
||||
/// <param name="authenticated">Whether the socket should authenticated</param>
|
||||
/// <param name="processor">The message processor requesting a connection</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct, IMessageProcessor? processor)
|
||||
{
|
||||
if (socket.Connected)
|
||||
return CallResult.Ok();
|
||||
@@ -533,16 +544,13 @@ namespace CryptoExchange.Net.Clients
|
||||
if (!connectResult.Success)
|
||||
return connectResult;
|
||||
|
||||
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
|
||||
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
||||
|
||||
if (!authenticated || socket.Authenticated)
|
||||
return CallResult.Ok();
|
||||
|
||||
if (socket is not SocketConnection sc)
|
||||
throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication");
|
||||
|
||||
var result = await AuthenticateSocketAsync(sc).ConfigureAwait(false);
|
||||
var result = await AuthenticateSocketAsync(sc, processor).ConfigureAwait(false);
|
||||
if (!result.Success)
|
||||
await socket.CloseAsync().ConfigureAwait(false);
|
||||
|
||||
@@ -554,13 +562,22 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
/// <param name="socket">Socket to authenticate</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
|
||||
public virtual Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
|
||||
=> AuthenticateSocketAsync(socket, null);
|
||||
|
||||
/// <summary>
|
||||
/// Authenticate a socket connection
|
||||
/// </summary>
|
||||
/// <param name="socket">Socket to authenticate</param>
|
||||
/// <param name="processor">The message processor requesting authentication</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket, IMessageProcessor? processor)
|
||||
{
|
||||
if (GetAuthenticationProvider() == null)
|
||||
return CallResult.Fail(new NoApiCredentialsError());
|
||||
|
||||
_logger.AttemptingToAuthenticate(socket.SocketId);
|
||||
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false);
|
||||
var authRequest = await GetAuthenticationRequestAsync(socket, processor).ConfigureAwait(false);
|
||||
if (authRequest != null)
|
||||
{
|
||||
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
|
||||
@@ -581,13 +598,21 @@ namespace CryptoExchange.Net.Clients
|
||||
return CallResult.Ok();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Should return the request which can be used to authenticate a socket connection
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) =>
|
||||
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) =>
|
||||
Task.FromResult(GetAuthenticationProvider()!.GetAuthenticationQuery(this, connection));
|
||||
|
||||
/// <summary>
|
||||
/// Should return the request which can be used to authenticate a socket connection
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection, IMessageProcessor? processor) =>
|
||||
GetAuthenticationRequestAsync(connection);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a system subscription. Used for example to reply to ping requests
|
||||
/// </summary>
|
||||
@@ -645,6 +670,18 @@ namespace CryptoExchange.Net.Clients
|
||||
&& (AllowTopicsOnTheSameConnection || !connection.Topics.Contains(topic));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the connection can be used for a new subscription or query with the provided parameters
|
||||
/// </summary>
|
||||
/// <param name="connection">The connection to check</param>
|
||||
/// <param name="address">The address set by the request</param>
|
||||
/// <param name="authenticated">Whether the request needs an authenticated connection</param>
|
||||
/// <param name="processor">The processor requesting a connection</param>
|
||||
/// <param name="topic">Topic of the request</param>
|
||||
/// <returns>True if connection can be used</returns>
|
||||
protected virtual bool ConnectionCanBeUsedFor(SocketConnection connection, string address, bool authenticated, IMessageProcessor? processor, string? topic = null)
|
||||
=> ConnectionCanBeUsedFor(connection, address, authenticated, topic);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
||||
/// </summary>
|
||||
@@ -655,15 +692,36 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
|
||||
/// <param name="individualSubscriptionCount">The number of individual subscriptions in this subscribe request</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Task<CallResult<SocketConnection>> GetSocketConnection(
|
||||
string address,
|
||||
bool authenticated,
|
||||
bool dedicatedRequestConnection,
|
||||
CancellationToken ct,
|
||||
string? topic = null,
|
||||
int individualSubscriptionCount = 1)
|
||||
=> GetSocketConnection(address, authenticated, dedicatedRequestConnection, ct, null, topic, individualSubscriptionCount);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
||||
/// </summary>
|
||||
/// <param name="address">The address the socket is for</param>
|
||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
|
||||
/// <param name="individualSubscriptionCount">The number of individual subscriptions in this subscribe request</param>
|
||||
/// <param name="processor">The processor requesting a connection</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(
|
||||
string address,
|
||||
bool authenticated,
|
||||
bool dedicatedRequestConnection,
|
||||
CancellationToken ct,
|
||||
IMessageProcessor? processor,
|
||||
string? topic = null,
|
||||
int individualSubscriptionCount = 1)
|
||||
{
|
||||
var socketQuery = _socketConnections.Where(s => ConnectionCanBeUsedFor(s.Value, address, authenticated, topic)).Select(x => x.Value); // Don't ToList this so the query is executed again when called
|
||||
var socketQuery = _socketConnections.Where(s => ConnectionCanBeUsedFor(s.Value, address, authenticated, processor, topic)).Select(x => x.Value); // Don't ToList this so the query is executed again when called
|
||||
|
||||
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
|
||||
var delayStart = DateTime.UtcNow;
|
||||
@@ -841,6 +899,8 @@ namespace CryptoExchange.Net.Clients
|
||||
return connectResult;
|
||||
}
|
||||
|
||||
// Remove the connection and stop any reconnect processing before disposing it.
|
||||
await socketConnection.CloseAsync().ConfigureAwait(false);
|
||||
socketConnection.Dispose();
|
||||
return connectResult;
|
||||
}
|
||||
@@ -862,7 +922,7 @@ namespace CryptoExchange.Net.Clients
|
||||
RateLimitAdmissionCallbackRequest = () => AdmissionOverride.Value,
|
||||
Proxy = ClientOptions.Proxy,
|
||||
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
|
||||
ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
|
||||
ReceiveBufferSize = ClientOptions.ReceiveBufferSize
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||
<PackageVersion>12.5.1</PackageVersion>
|
||||
<AssemblyVersion>12.5.1</AssemblyVersion>
|
||||
<FileVersion>12.5.1</FileVersion>
|
||||
<PackageVersion>13.2.3</PackageVersion>
|
||||
<AssemblyVersion>13.2.3</AssemblyVersion>
|
||||
<FileVersion>13.2.3</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
|
||||
@@ -130,7 +130,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// Default error info
|
||||
/// </summary>
|
||||
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.MissingCredentials, false,
|
||||
protected static readonly ErrorInfo _errorInfo = new ErrorInfo(ErrorType.MissingCredentials, false,
|
||||
"No credentials provided for private endpoint, set the `ApiCredentials` option in the client configuration");
|
||||
|
||||
/// <summary>
|
||||
@@ -138,6 +138,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public NoApiCredentialsError() : base(null, _errorInfo, null) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public NoApiCredentialsError(string message) : base(null, _errorInfo with { Message = message }, null) { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
|
||||
@@ -19,6 +19,11 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </summary>
|
||||
public int? MaxSocketConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time to wait after connecting a socket before sending messages. Can be used for API's which will rate limit if you subscribe directly after connecting.
|
||||
/// </summary>
|
||||
public TimeSpan? DelayAfterConnect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
@@ -28,6 +33,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
item.SocketNoDataTimeout = SocketNoDataTimeout;
|
||||
item.AutoTimestamp = AutoTimestamp;
|
||||
item.MaxSocketConnections = MaxSocketConnections;
|
||||
item.DelayAfterConnect = DelayAfterConnect;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Get user funding history capability.
|
||||
/// </summary>
|
||||
public static SharedRestCapability<IGetUserFundingHistory, IGetUserFundingHistoryRest> GetUserFundingHistory { get; } = new();
|
||||
/// <summary>
|
||||
/// Subscribe to funding info updates capability.
|
||||
/// </summary>
|
||||
public static SharedCapabilityReference<ISubscribeFundingInfoSocket> SubscribeFundingInfo { get; } = new();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Order book info
|
||||
/// </summary>
|
||||
public record SharedIncrementalOrderBook : SharedOrderBook
|
||||
{
|
||||
/// <summary>
|
||||
/// The sequence number of the first book update in this update.
|
||||
/// </summary>
|
||||
public long? StartSequenceNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SharedIncrementalOrderBook(SharedQuantityType quantityType, long? startSequenceNumber, long? endSequenceNumber, ISymbolOrderBookEntry[] asks, ISymbolOrderBookEntry[] bids)
|
||||
:base(quantityType, endSequenceNumber, asks, bids)
|
||||
{
|
||||
StartSequenceNumber = startSequenceNumber;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
@@ -104,6 +105,11 @@ namespace CryptoExchange.Net.SharedApis
|
||||
SharedCapabilityResolution<T>? GetCapability<T>(SharedCapabilityReference<T> capability, TradingMode tradingMode)
|
||||
where T : ISharedApiCapability;
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes all subscriptions created by the underlying socket API
|
||||
/// client and closes its subscription connections.
|
||||
/// </summary>
|
||||
Task UnsubscribeAllAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -209,6 +215,15 @@ namespace CryptoExchange.Net.SharedApis
|
||||
return GetCapabilities<T>(tradingMode);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeAllAsync()
|
||||
{
|
||||
return Task.WhenAll(
|
||||
_sharedApis
|
||||
.OfType<ISharedSubscription>()
|
||||
.Select(x => x.UnsubscribeAllAsync()));
|
||||
}
|
||||
|
||||
object? ISharedApiClientResolver.GetCapability(Type capabilityType)
|
||||
{
|
||||
return _sharedApis
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
@@ -624,5 +626,212 @@ namespace CryptoExchange.Net.SharedApis
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to trade updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeTradesSocket>> capabilities,
|
||||
SubscribeTradeRequest request,
|
||||
Action<DataEvent<SharedTrade[]>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToTradeUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to balance updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeBalancesSocket>> capabilities,
|
||||
SubscribeBalancesRequest request,
|
||||
Action<DataEvent<SharedBalance[]>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToBalanceUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to index price updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeIndexPriceSocket>> capabilities,
|
||||
SubscribeIndexPriceRequest request,
|
||||
Action<DataEvent<SharedIndexPrice>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToIndexPriceUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to funding info updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeFundingInfoSocket>> capabilities,
|
||||
SubscribeFundingInfoRequest request,
|
||||
Action<DataEvent<SharedFundingInfo>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToFundingInfoUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to kline updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeKlinesSocket>> capabilities,
|
||||
SubscribeKlineRequest request,
|
||||
Action<DataEvent<SharedKline>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToKlineUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to mark price updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeMarkPriceSocket>> capabilities,
|
||||
SubscribeMarkPriceRequest request,
|
||||
Action<DataEvent<SharedMarkPrice>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToMarkPriceUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to order book updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeOrderBookSocket>> capabilities,
|
||||
SubscribeOrderBookRequest request,
|
||||
Action<DataEvent<SharedOrderBook>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToOrderBookUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to incremental order book updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeIncrementalOrderBookSocket>> capabilities,
|
||||
SubscribeOrderBookRequest request,
|
||||
Action<DataEvent<SharedIncrementalOrderBook>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToIncrementalOrderBookUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to futures order updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeFuturesOrdersSocket>> capabilities,
|
||||
SubscribeFuturesOrderRequest request,
|
||||
Action<DataEvent<SharedFuturesOrderUpdate[]>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToFuturesOrderUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to spot order updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeSpotOrdersSocket>> capabilities,
|
||||
SubscribeSpotOrderRequest request,
|
||||
Action<DataEvent<SharedSpotOrderUpdate[]>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToSpotOrderUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to position updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribePositionsSocket>> capabilities,
|
||||
SubscribePositionRequest request,
|
||||
Action<DataEvent<SharedPosition[]>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToPositionUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to ticker updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeTickerSocket>> capabilities,
|
||||
SubscribeTickerRequest request,
|
||||
Action<DataEvent<SharedTicker>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToTickerUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to all ticker updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeAllTickersSocket>> capabilities,
|
||||
SubscribeAllTickersRequest request,
|
||||
Action<DataEvent<SharedTicker[]>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToAllTickersUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to user trade updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeUserTradesSocket>> capabilities,
|
||||
SubscribeUserTradeRequest request,
|
||||
Action<DataEvent<SharedUserTrade[]>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToUserTradeUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for all results from an IAsyncEnumerable and return them as an array
|
||||
/// </summary>
|
||||
public static async Task<T[]> WaitAllAsync<T>(this IAsyncEnumerable<T> enumerable)
|
||||
{
|
||||
var result = new List<T>();
|
||||
await foreach (var itemResult in enumerable.ConfigureAwait(false))
|
||||
result.Add(itemResult);
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Operation for subscribing to funding info updates
|
||||
/// </summary>
|
||||
public interface ISubscribeFundingInfoSocket : ISharedSubscription
|
||||
{
|
||||
/// <summary>
|
||||
/// Funding info subscription options
|
||||
/// </summary>
|
||||
SubscribeFundingInfoOptions SubscribeFundingInfoOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to funding info updates
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="handler">Update handler</param>
|
||||
/// <param name="ct">Cancellation token, can be used to stop the updates</param>
|
||||
/// <returns></returns>
|
||||
Task<WebSocketResult<UpdateSubscription>> SubscribeToFundingInfoUpdatesAsync(SubscribeFundingInfoRequest request, Action<DataEvent<SharedFundingInfo>> handler, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for subscribing to funding info updates
|
||||
/// </summary>
|
||||
public class SubscribeFundingInfoOptions : CapabilityOptions<SubscribeFundingInfoRequest, ISubscribeFundingInfoSocket>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string Description => "Subscribe to funding info updates for a symbol";
|
||||
|
||||
private static readonly RequestParameterDescription[] _defaultParameterRules = new[]
|
||||
{
|
||||
RequestParameterRule<SubscribeFundingInfoRequest>.Optional(x => x.Symbol, "The symbol to subscribe to", new SharedSymbol(TradingMode.PerpetualLinear, "ETH", "USDT")),
|
||||
RequestParameterRule<SubscribeFundingInfoRequest>.Optional(x => x.Symbols, "The symbols to subscribe to", new[] { new SharedSymbol(TradingMode.PerpetualLinear, "ETH", "USDT") }),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SubscribeFundingInfoOptions(string exchange, bool needsAuthentication)
|
||||
: base(exchange, needsAuthentication, nameof(ISubscribeFundingInfoSocket.SubscribeToFundingInfoUpdatesAsync), _defaultParameterRules, SharedTradingModeSets.Futures)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Request to subscribe to funding info updates for a symbol
|
||||
/// </summary>
|
||||
public record SubscribeFundingInfoRequest : SharedSymbolRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to subscribe to</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public SubscribeFundingInfoRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null)
|
||||
: base(symbol, exchangeParameters)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="symbols">The symbols to subscribe to</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public SubscribeFundingInfoRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null)
|
||||
: base(symbols, exchangeParameters)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared WebSocket API subscription capabitlity
|
||||
/// Shared WebSocket API subscription capability
|
||||
/// </summary>
|
||||
public interface ISharedSubscription : ISharedSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// Unsubscribes all subscriptions created by the underlying socket API
|
||||
/// client and closes its subscription connections.
|
||||
/// </summary>
|
||||
Task UnsubscribeAllAsync();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// Order book subscription options
|
||||
/// </summary>
|
||||
SubscribeOrderBookOptions SubscribeOrderBookOptions { get; }
|
||||
SubscribeIncrementalOrderBookOptions SubscribeIncrementalOrderBookOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to incremental order book updates for a symbol
|
||||
@@ -23,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="handler">Update handler</param>
|
||||
/// <param name="ct">Cancellation token, can be used to stop the updates</param>
|
||||
/// <returns></returns>
|
||||
Task<WebSocketResult<UpdateSubscription>> SubscribeToOrderBookUpdatesAsync(SubscribeOrderBookRequest request, Action<DataEvent<SharedOrderBook>> handler, CancellationToken ct = default);
|
||||
Task<WebSocketResult<UpdateSubscription>> SubscribeToIncrementalOrderBookUpdatesAsync(SubscribeOrderBookRequest request, Action<DataEvent<SharedIncrementalOrderBook>> handler, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SubscribeIncrementalOrderBookOptions(string exchange, bool needsAuthentication, int[] limits, SharedOrderBookSubscriptionType updateType)
|
||||
: base(exchange, needsAuthentication, nameof(ISubscribeIncrementalOrderBookSocket.SubscribeToOrderBookUpdatesAsync), _defaultParameterRules)
|
||||
: base(exchange, needsAuthentication, nameof(ISubscribeIncrementalOrderBookSocket.SubscribeToIncrementalOrderBookUpdatesAsync), _defaultParameterRules)
|
||||
{
|
||||
SupportedLimits = limits;
|
||||
UpdateType = updateType;
|
||||
|
||||
@@ -446,18 +446,16 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
if (_closeTask?.IsCompleted == false)
|
||||
{
|
||||
_logger.SocketCloseAsyncWaitingForExistingCloseTask(Id);
|
||||
await _closeTask.ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsOpen)
|
||||
else
|
||||
{
|
||||
_logger.SocketCloseAsyncSocketNotOpen(Id);
|
||||
return;
|
||||
}
|
||||
if (!IsOpen)
|
||||
_logger.SocketCloseAsyncSocketNotOpen(Id);
|
||||
else
|
||||
_logger.SocketClosing(Id);
|
||||
|
||||
_logger.SocketClosing(Id);
|
||||
_closeTask = CloseInternalAsync();
|
||||
_closeTask = CloseInternalAsync();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -465,8 +463,11 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
}
|
||||
|
||||
await _closeTask.ConfigureAwait(false);
|
||||
|
||||
// Processing may still be running after the socket closes; finish before the caller disposes it.
|
||||
if(_processTask != null)
|
||||
await _processTask.ConfigureAwait(false);
|
||||
|
||||
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
_logger.SocketClosed(Id);
|
||||
}
|
||||
|
||||
@@ -424,6 +424,9 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
{
|
||||
try
|
||||
{
|
||||
if ((ApiClient.ApiOptions.DelayAfterConnect ?? ApiClient.ClientOptions.DelayAfterConnect) != TimeSpan.Zero)
|
||||
await Task.Delay(ApiClient.ApiOptions.DelayAfterConnect ?? ApiClient.ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
||||
|
||||
var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false);
|
||||
if (!reconnectSuccessful.Success)
|
||||
{
|
||||
@@ -616,7 +619,17 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
/// Connect the websocket
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<CallResult> ConnectAsync(CancellationToken ct) => await _socket.ConnectAsync(ct).ConfigureAwait(false);
|
||||
public async Task<CallResult> ConnectAsync(CancellationToken ct)
|
||||
{
|
||||
var result = await _socket.ConnectAsync(ct).ConfigureAwait(false);
|
||||
if (!result.Success)
|
||||
return result;
|
||||
|
||||
if ((ApiClient.ApiOptions.DelayAfterConnect ?? ApiClient.ClientOptions.DelayAfterConnect) != TimeSpan.Zero)
|
||||
await Task.Delay(ApiClient.ApiOptions.DelayAfterConnect ?? ApiClient.ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the underlying socket
|
||||
@@ -689,7 +702,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
if (subscription.CancellationTokenRegistration.HasValue)
|
||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||
|
||||
bool anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.MessageRouter.Routes.All(l => subscription.MessageRouter.ContainsCheck(l)));
|
||||
bool anyDuplicateSubscription = SubscriptionIsCovered(subscription);
|
||||
bool shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Status == SubscriptionStatus.Closing || r.Status == SubscriptionStatus.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
|
||||
|
||||
if (!anyDuplicateSubscription)
|
||||
@@ -1056,6 +1069,36 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
return CallResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether there is currently a subscription covering all the routes the subscription lists
|
||||
/// </summary>
|
||||
protected bool SubscriptionIsCovered(Subscription subscription)
|
||||
=> _listeners.OfType<Subscription>().Any(
|
||||
x => x != subscription
|
||||
&& x.Active
|
||||
&& subscription.MessageRouter.Routes.All(l => x.MessageRouter.ContainsCheck(l)));
|
||||
|
||||
/// <summary>
|
||||
/// Whether there is a subscription earlier in the list covering all the routes the subscription lists
|
||||
/// </summary>
|
||||
protected bool IsCoveredByEarlierSubscription(Subscription subscription)
|
||||
{
|
||||
if (subscription.MessageRouter.Routes.Length == 0)
|
||||
// Can be subscription which only knows what to listen to after receiving the subscribe response, for example Bitfinex
|
||||
return false;
|
||||
|
||||
foreach (var other in _listeners.OfType<Subscription>())
|
||||
{
|
||||
if (other == subscription)
|
||||
return false;
|
||||
|
||||
if (other.Active && subscription.MessageRouter.Routes.All(x => other.MessageRouter.ContainsCheck(x)))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to subscribe a new subscription by sending the subscribe query and wait for the result as needed
|
||||
/// </summary>
|
||||
@@ -1090,11 +1133,11 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
return WebSocketResult.Ok(ApiClient.Exchange, SocketId, default, 0, ConnectionUriString);
|
||||
}
|
||||
|
||||
var subCompleteHandler = () =>
|
||||
var subCompleteHandler = (bool success) =>
|
||||
{
|
||||
subscription.Status = subQuery.Success ? SubscriptionStatus.Subscribed : SubscriptionStatus.Pending;
|
||||
subscription.Status = success ? SubscriptionStatus.Subscribed : SubscriptionStatus.Pending;
|
||||
subscription.HandleSubQueryResponse(this, subQuery.Response);
|
||||
if (newSubscription && subQuery.Success)
|
||||
if (newSubscription && success)
|
||||
{
|
||||
subscription.TokenExpired += HandleTokenExpired;
|
||||
if (subCancelToken != default)
|
||||
@@ -1109,6 +1152,14 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
};
|
||||
subQuery.OnComplete = subCompleteHandler;
|
||||
|
||||
// For duplicate subscription don't send another subscribe request but just handle it internally
|
||||
// Unsubscribe also doesn't unsubscribe when there is still a duplicate subscription running
|
||||
if (IsCoveredByEarlierSubscription(subscription))
|
||||
{
|
||||
subQuery.Complete();
|
||||
return WebSocketResult.Ok(ApiClient.Exchange, SocketId, default, subQuery.Id, ConnectionUriString);
|
||||
}
|
||||
|
||||
var subQueryResult = await SendAndWaitQueryAsync(subQuery).ConfigureAwait(false);
|
||||
if (!subQueryResult.Success)
|
||||
{
|
||||
@@ -1119,9 +1170,6 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
return WebSocketResult.Fail(ApiClient.Exchange, subQueryResult.Error!);
|
||||
}
|
||||
|
||||
if (!subQuery.ExpectsResponse)
|
||||
subCompleteHandler();
|
||||
|
||||
return subQueryResult;
|
||||
}
|
||||
|
||||
|
||||
@@ -198,8 +198,13 @@ namespace CryptoExchange.Net.Sockets.HighPerf
|
||||
{
|
||||
var result = await _socket.ConnectAsync(ct).ConfigureAwait(false);
|
||||
if (result.Success)
|
||||
{
|
||||
_processTask = ProcessAsync(_cts.Token);
|
||||
|
||||
if ((ApiClient.ApiOptions.DelayAfterConnect ?? ApiClient.ClientOptions.DelayAfterConnect) != TimeSpan.Zero)
|
||||
await Task.Delay(ApiClient.ApiOptions.DelayAfterConnect ?? ApiClient.ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <summary>
|
||||
/// On complete callback
|
||||
/// </summary>
|
||||
public Action? OnComplete { get; set; }
|
||||
public Action<bool>? OnComplete { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action? OnMessageRouterUpdated;
|
||||
@@ -157,8 +157,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
else
|
||||
{
|
||||
Completed = true;
|
||||
_event.Set();
|
||||
Complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +174,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public abstract void Timeout();
|
||||
|
||||
/// <summary>
|
||||
/// Mark request as successfully completed
|
||||
/// </summary>
|
||||
public abstract void Complete();
|
||||
|
||||
/// <summary>
|
||||
/// Mark request as failed
|
||||
/// </summary>
|
||||
@@ -248,7 +252,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
Completed = true;
|
||||
_event.Set();
|
||||
OnComplete?.Invoke();
|
||||
OnComplete?.Invoke(Success);
|
||||
}
|
||||
|
||||
return handled;
|
||||
@@ -267,7 +271,19 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
Completed = true;
|
||||
_event.Set();
|
||||
OnComplete?.Invoke();
|
||||
OnComplete?.Invoke(Success);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Complete()
|
||||
{
|
||||
if (Completed)
|
||||
return;
|
||||
|
||||
Result = CallResult<THandlerResponse>.Ok(default!);
|
||||
Completed = true;
|
||||
_event.Set();
|
||||
OnComplete?.Invoke(Success);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -280,7 +296,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
Completed = true;
|
||||
|
||||
_event.Set();
|
||||
OnComplete?.Invoke();
|
||||
OnComplete?.Invoke(Success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,12 +168,18 @@ namespace CryptoExchange.Net.Testing
|
||||
{
|
||||
// Validate request body
|
||||
Dictionary<string, object> bodyParameters;
|
||||
if (result.RequestBody!.StartsWith("{") || result.RequestBody.StartsWith("["))
|
||||
if (string.IsNullOrEmpty(result.RequestBody))
|
||||
{
|
||||
bodyParameters = new Dictionary<string, object>();
|
||||
}
|
||||
else if (result.RequestBody!.StartsWith("{") || result.RequestBody.StartsWith("["))
|
||||
{
|
||||
// Json
|
||||
bodyParameters = JsonSerializer.Deserialize<Dictionary<string, object>>(result.RequestBody!)!;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Form data
|
||||
var splitKvp = result.RequestBody.Split('&');
|
||||
bodyParameters = splitKvp.Select(x => x.Split('=')).ToDictionary(x => x[0], x => (object)x[1]);
|
||||
}
|
||||
@@ -196,7 +202,7 @@ namespace CryptoExchange.Net.Testing
|
||||
private void CompareParameters(Dictionary<string, object> expectedUrlParameters, Dictionary<string, object> parameters, List<string>? ignoreParamValidation)
|
||||
{
|
||||
if (expectedUrlParameters.Count > parameters.Count)
|
||||
throw new Exception($"Url parameters count not matched. Expected: {expectedUrlParameters.Count}, Actual: {parameters.Count}");
|
||||
throw new Exception($"Parameters count not matched. Expected: {expectedUrlParameters.Count}, Actual: {parameters.Count}");
|
||||
|
||||
foreach (var kvp in expectedUrlParameters)
|
||||
{
|
||||
@@ -204,10 +210,10 @@ namespace CryptoExchange.Net.Testing
|
||||
continue;
|
||||
|
||||
if (!parameters.TryGetValue(kvp.Key, out var value))
|
||||
throw new Exception($"Url parameter {kvp.Key} not found in actual parameters");
|
||||
throw new Exception($"Parameter {kvp.Key} not found in actual parameters");
|
||||
|
||||
if (Convert.ToString(kvp.Value, CultureInfo.InvariantCulture) != Convert.ToString(value, CultureInfo.InvariantCulture))
|
||||
throw new Exception($"Url parameter {kvp.Key} value not matched. Expected: {kvp.Value}, Actual: {value}");
|
||||
throw new Exception($"Parameter {kvp.Key} value not matched. Expected: {kvp.Value}, Actual: {value}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,10 +28,11 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
SharedAccountType accountType,
|
||||
TrackerItemConfig config,
|
||||
ExchangeParameters? exchangeParameters = null
|
||||
) : base(logger, symbolTracker, UserDataType.Balances, restClient.Exchange, config)
|
||||
) : base(logger, symbolTracker, UserDataType.Balances, restClient.Exchange)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
if (socketClient == null)
|
||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||
SetConfig(config);
|
||||
|
||||
_restClient = restClient;
|
||||
_socketClient = socketClient;
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
private readonly Dictionary<string, int> _openOrderNotReturnedTimes = new();
|
||||
private readonly TimeSpan _pollOverlapPeriod = TimeSpan.FromSeconds(3);
|
||||
|
||||
private bool _openOrderWarningDone = false;
|
||||
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
|
||||
|
||||
/// <summary>
|
||||
@@ -39,10 +40,11 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
IEnumerable<SharedSymbol> symbols,
|
||||
bool onlyTrackProvidedSymbols,
|
||||
ExchangeParameters? exchangeParameters = null
|
||||
) : base(logger, symbolTracker, UserDataType.Orders, openRestClient.Exchange, config)
|
||||
) : base(logger, symbolTracker, UserDataType.Orders, openRestClient.Exchange)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
if (socketClient == null)
|
||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||
SetConfig(config);
|
||||
|
||||
_openOrdersClient = openRestClient;
|
||||
_closedOrdersClient = closedRestClient;
|
||||
@@ -234,7 +236,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
|
||||
var trackedSymbols = _symbolTracker.GetTrackedSymbols();
|
||||
if (!_openOrderWarningDone && trackedSymbols.Count() == 0)
|
||||
{
|
||||
_openOrderWarningDone = true;
|
||||
_logger.LogDebug("No tracked symbols specified while symbol is required to retrieve open orders. Tracked symbols should be updated before orders may be returned");
|
||||
}
|
||||
|
||||
foreach (var symbol in trackedSymbols)
|
||||
{
|
||||
var openOrdersResult = await _openOrdersClient.GetOpenFuturesOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (!openOrdersResult.Success)
|
||||
|
||||
@@ -34,10 +34,11 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
IEnumerable<SharedSymbol> symbols,
|
||||
bool onlyTrackProvidedSymbols,
|
||||
ExchangeParameters? exchangeParameters = null
|
||||
) : base(logger, symbolTracker, UserDataType.Trades, restClient.Exchange, config)
|
||||
) : base(logger, symbolTracker, UserDataType.Trades, restClient.Exchange)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
if (socketClient == null)
|
||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||
SetConfig(config);
|
||||
|
||||
_restClient = restClient;
|
||||
_socketClient = socketClient;
|
||||
|
||||
@@ -37,10 +37,11 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
bool onlyTrackProvidedSymbols,
|
||||
bool websocketPositionUpdatesAreFullSnapshots,
|
||||
ExchangeParameters? exchangeParameters = null
|
||||
) : base(logger, symbolTracker, UserDataType.Positions, restClient.Exchange, config)
|
||||
) : base(logger, symbolTracker, UserDataType.Positions, restClient.Exchange)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
if (socketClient == null)
|
||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||
SetConfig(config);
|
||||
|
||||
_restClient = restClient;
|
||||
_socketClient = socketClient;
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
private readonly bool _timeFilterSupportedClosedOrders;
|
||||
private readonly Dictionary<string, int> _openOrderNotReturnedTimes = new();
|
||||
private readonly TimeSpan _pollOverlapPeriod = TimeSpan.FromSeconds(3);
|
||||
private bool _openOrderWarningDone = false;
|
||||
|
||||
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
|
||||
|
||||
@@ -39,10 +40,11 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
IEnumerable<SharedSymbol> symbols,
|
||||
bool onlyTrackProvidedSymbols,
|
||||
ExchangeParameters? exchangeParameters = null
|
||||
) : base(logger, symbolTracker, UserDataType.Orders, openOrderClient.Exchange, config)
|
||||
) : base(logger, symbolTracker, UserDataType.Orders, openOrderClient.Exchange)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
if (socketClient == null)
|
||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||
SetConfig(config);
|
||||
|
||||
_openOrderClient = openOrderClient;
|
||||
_closedOrderClient = closedOrderClient;
|
||||
@@ -244,7 +246,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
|
||||
var trackedSymbols = _symbolTracker.GetTrackedSymbols();
|
||||
if (!_openOrderWarningDone && trackedSymbols.Count() == 0)
|
||||
{
|
||||
_openOrderWarningDone = true;
|
||||
_logger.LogDebug("No tracked symbols specified while symbol is required to retrieve open orders. Tracked symbols should be updated before orders may be returned");
|
||||
}
|
||||
|
||||
foreach (var symbol in trackedSymbols)
|
||||
{
|
||||
var openOrdersResult = await _openOrderClient.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (!openOrdersResult.Success)
|
||||
|
||||
@@ -34,10 +34,11 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
IEnumerable<SharedSymbol> symbols,
|
||||
bool onlyTrackProvidedSymbols,
|
||||
ExchangeParameters? exchangeParameters = null
|
||||
) : base(logger, symbolTracker, UserDataType.Trades, restClient.Exchange, config)
|
||||
) : base(logger, symbolTracker, UserDataType.Trades, restClient.Exchange)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
if (socketClient == null)
|
||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||
SetConfig(config);
|
||||
|
||||
_restClient = restClient;
|
||||
_socketClient = socketClient;
|
||||
|
||||
@@ -242,15 +242,21 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
ILogger logger,
|
||||
UserDataSymbolTracker symbolTracker,
|
||||
UserDataType dataType,
|
||||
string exchange,
|
||||
TrackerItemConfig config) : base(logger, dataType, exchange)
|
||||
string exchange) : base(logger, dataType, exchange)
|
||||
{
|
||||
_isSymbolModel = typeof(T).IsSubclassOf(typeof(SharedSymbolModel));
|
||||
_symbolTracker = symbolTracker;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the tracker item config
|
||||
/// </summary>
|
||||
protected void SetConfig(TrackerItemConfig config)
|
||||
{
|
||||
_pollIntervalDisconnected = config.PollIntervalDisconnected;
|
||||
_pollIntervalConnected = config.PollIntervalConnected;
|
||||
_pollAtStart = config.PollAtStart;
|
||||
_retentionTime = config is TrackerTimedItemConfig timeConfig ? timeConfig.RetentionTime : TimeSpan.MaxValue;
|
||||
_isSymbolModel = typeof(T).IsSubclassOf(typeof(SharedSymbolModel));
|
||||
_symbolTracker = symbolTracker;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -5,37 +5,36 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="13.5.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="11.4.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="4.4.0" />
|
||||
<PackageReference Include="BloFin.Net" Version="3.4.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="7.5.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="11.4.0" />
|
||||
<PackageReference Include="CoinW.Net" Version="3.4.0" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="4.4.0" />
|
||||
<PackageReference Include="DeepCoin.Net" Version="4.4.0" />
|
||||
<PackageReference Include="GateIo.Net" Version="4.6.0" />
|
||||
<PackageReference Include="HyperLiquid.Net" Version="5.7.0" />
|
||||
<PackageReference Include="JK.BingX.Net" Version="4.5.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="4.5.0" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="6.6.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="5.4.0" />
|
||||
<PackageReference Include="Jkorf.Aster.Net" Version="4.4.0" />
|
||||
<PackageReference Include="JKorf.BitMEX.Net" Version="4.4.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="4.5.0" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="9.5.0" />
|
||||
<PackageReference Include="JKorf.Lighter.Net" Version="1.5.0" />
|
||||
<PackageReference Include="JKorf.Upbit.Net" Version="3.4.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="8.4.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="9.5.0" />
|
||||
<PackageReference Include="LBank.Net" Version="1.1.0" />
|
||||
<PackageReference Include="Pionex.Net" Version="1.2.0" />
|
||||
<PackageReference Include="Binance.Net" Version="13.7.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="11.7.1" />
|
||||
<PackageReference Include="Bitstamp.Net" Version="2.6.0" />
|
||||
<PackageReference Include="Bitunix.Net" Version="1.0.1" />
|
||||
<PackageReference Include="BloFin.Net" Version="3.6.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="7.7.0" />
|
||||
<PackageReference Include="CoinW.Net" Version="3.6.2" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="4.6.0" />
|
||||
<PackageReference Include="DeepCoin.Net" Version="4.6.1" />
|
||||
<PackageReference Include="GateIo.Net" Version="4.8.0" />
|
||||
<PackageReference Include="HyperLiquid.Net" Version="5.9.1" />
|
||||
<PackageReference Include="JK.BingX.Net" Version="4.7.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="4.9.0" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="6.8.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="5.7.0" />
|
||||
<PackageReference Include="Jkorf.Aster.Net" Version="4.6.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="4.7.0" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="9.7.0" />
|
||||
<PackageReference Include="JKorf.Lighter.Net" Version="1.7.0" />
|
||||
<PackageReference Include="JKorf.Upbit.Net" Version="3.6.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="8.6.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="9.8.0" />
|
||||
<PackageReference Include="LBank.Net" Version="1.3.0" />
|
||||
<PackageReference Include="Pionex.Net" Version="1.4.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageReference Include="Tapbit.Net" Version="1.0.0" />
|
||||
<PackageReference Include="Toobit.Net" Version="4.4.0" />
|
||||
<PackageReference Include="Weex.Net" Version="2.4.0" />
|
||||
<PackageReference Include="WhiteBit.Net" Version="4.4.0" />
|
||||
<PackageReference Include="XT.Net" Version="4.5.0" />
|
||||
<PackageReference Include="Tapbit.Net" Version="1.2.0" />
|
||||
<PackageReference Include="Toobit.Net" Version="4.6.0" />
|
||||
<PackageReference Include="Weex.Net" Version="2.6.0" />
|
||||
<PackageReference Include="WhiteBit.Net" Version="4.7.0" />
|
||||
<PackageReference Include="XT.Net" Version="4.7.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
@page "/"
|
||||
@using Bitstamp.Net.Interfaces.Clients
|
||||
@using Bitunix.Net.Interfaces.Clients
|
||||
@inject IAsterRestClient asterClient
|
||||
@inject IBinanceRestClient binanceClient
|
||||
@inject IBingXRestClient bingXClient
|
||||
@inject IBitfinexRestClient bitfinexClient
|
||||
@inject IBitgetRestClient bitgetClient
|
||||
@inject IBitMartRestClient bitmartClient
|
||||
@inject IBitMEXRestClient bitmexClient
|
||||
@inject IBitstampRestClient bitstampClient
|
||||
@inject IBitunixRestClient bitunixClient
|
||||
@inject IBloFinRestClient bloFinClient
|
||||
@inject IBybitRestClient bybitClient
|
||||
@inject ICoinbaseRestClient coinbaseClient
|
||||
@inject ICoinExRestClient coinexClient
|
||||
@inject ICoinWRestClient coinWClient
|
||||
@inject ICryptoComRestClient cryptocomClient
|
||||
@inject IDeepCoinRestClient deepCoinClient
|
||||
@@ -46,12 +47,11 @@
|
||||
var bingXTask = bingXClient.SpotApi.ExchangeData.GetTickersAsync("BTC-USDT");
|
||||
var bitfinexTask = bitfinexClient.ExchangeApi.ExchangeData.GetTickerAsync("tBTCUSD");
|
||||
var bitgetTask = bitgetClient.SpotApiV2.ExchangeData.GetTickersAsync("BTCUSDT");
|
||||
var bitmartTask = bitmartClient.SpotApi.ExchangeData.GetTickerAsync("BTC_USDT");
|
||||
var bitmexTask = bitmexClient.ExchangeApi.ExchangeData.GetSymbolsAsync("XBT_USDT");
|
||||
var bitstampTask = bitstampClient.ExchangeApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var bitunixTask = bitunixClient.SpotApi.ExchangeData.GetLastPriceAsync("BTCUSDT");
|
||||
var bloFinTask = bloFinClient.FuturesApi.ExchangeData.GetTickersAsync("BTC-USDT");
|
||||
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
|
||||
var coinbaseTask = coinbaseClient.AdvancedTradeApi.ExchangeData.GetSymbolAsync("BTC-USDT");
|
||||
var coinexTask = coinexClient.SpotApiV2.ExchangeData.GetTickersAsync(["BTCUSDT"]);
|
||||
var coinWTask = coinWClient.SpotApi.ExchangeData.GetTickersAsync();
|
||||
var cryptocomTask = cryptocomClient.ExchangeApi.ExchangeData.GetTickersAsync("BTC_USDT");
|
||||
var deepCoinTask = deepCoinClient.ExchangeApi.ExchangeData.GetTickersAsync(DeepCoin.Net.Enums.SymbolType.Spot);
|
||||
@@ -71,7 +71,7 @@
|
||||
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
|
||||
var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt");
|
||||
|
||||
await Task.WhenAll(asterTask, binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bloFinTask, bitmexTask, bybitTask, coinexTask, coinWTask, deepCoinTask, gateioTask,
|
||||
await Task.WhenAll(asterTask, binanceTask, bingXTask, bitfinexTask, bitgetTask, bitstampTask, bitunixTask, bloFinTask, bybitTask, coinWTask, deepCoinTask, gateioTask,
|
||||
htxTask, krakenTask, kucoinTask, lBankTask, mexcTask, okxTask, pionexTask, toobitTask, upbitTask, weexTask, whitebitTask, xtTask);
|
||||
|
||||
if (asterTask.Result.Success)
|
||||
@@ -89,11 +89,11 @@
|
||||
if (bitgetTask.Result.Success)
|
||||
_prices.Add("Bitget", bitgetTask.Result.Data.Single().LastPrice);
|
||||
|
||||
if (bitmartTask.Result.Success)
|
||||
_prices.Add("BitMart", bitmartTask.Result.Data.LastPrice);
|
||||
if (bitstampTask.Result.Success)
|
||||
_prices.Add("Bitstamp", bitstampTask.Result.Data.LastPrice);
|
||||
|
||||
if (bitmexTask.Result.Success)
|
||||
_prices.Add("BitMEX", bitmexTask.Result.Data.First().LastPrice);
|
||||
if (bitunixTask.Result.Success)
|
||||
_prices.Add("Bitunix", bitunixTask.Result.Data);
|
||||
|
||||
if (bloFinTask.Result.Success)
|
||||
_prices.Add("BloFin", bloFinTask.Result.Data.First().LastPrice);
|
||||
@@ -104,9 +104,6 @@
|
||||
if (coinbaseTask.Result.Success)
|
||||
_prices.Add("Coinbase", coinbaseTask.Result.Data.LastPrice ?? 0);
|
||||
|
||||
if (coinexTask.Result.Success)
|
||||
_prices.Add("CoinEx", coinexTask.Result.Data.Single().LastPrice);
|
||||
|
||||
if (coinWTask.Result.Success)
|
||||
_prices.Add("CoinW", coinWTask.Result.Data.Single(x => x.Symbol == "BTC_USDT").LastPrice);
|
||||
|
||||
|
||||
@@ -4,12 +4,11 @@
|
||||
@inject IBingXSocketClient bingXSocketClient
|
||||
@inject IBitfinexSocketClient bitfinexSocketClient
|
||||
@inject IBitgetSocketClient bitgetSocketClient
|
||||
@inject IBitMartSocketClient bitmartSocketClient
|
||||
@inject IBitMEXSocketClient bitmexSocketClient
|
||||
@inject IBitstampSocketClient bitstampSocketClient
|
||||
@inject IBitunixSocketClient bitunixSocketClient
|
||||
@inject IBloFinSocketClient bloFinSocketClient
|
||||
@inject IBybitSocketClient bybitSocketClient
|
||||
@inject ICoinbaseSocketClient coinbaseSocketClient
|
||||
@inject ICoinExSocketClient coinExSocketClient
|
||||
@inject ICoinWSocketClient coinWSocketClient
|
||||
@inject ICryptoComSocketClient cryptocomSocketClient
|
||||
@inject IDeepCoinSocketClient deepCoinSocketClient
|
||||
@@ -29,6 +28,8 @@
|
||||
@inject IWhiteBitSocketClient whitebitSocketClient
|
||||
@inject IXTSocketClient xtSocketClient
|
||||
@using System.Collections.Concurrent
|
||||
@using Bitstamp.Net.Interfaces.Clients
|
||||
@using Bitunix.Net.Interfaces.Clients
|
||||
@using CryptoExchange.Net.Objects
|
||||
@using CryptoExchange.Net.Objects.Sockets;
|
||||
@using CryptoExchange.Net.Sockets
|
||||
@@ -55,12 +56,9 @@
|
||||
bingXSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BingX", data.Data.LastPrice)),
|
||||
bitfinexSocketClient.ExchangeApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
|
||||
bitgetSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.First().LastPrice)),
|
||||
bitmartSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("BitMart", data.Data.LastPrice)),
|
||||
bitmexSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH_XBT", data => UpdateData("BitMEX", data.Data.LastPrice ?? 0)),
|
||||
// BloFin doesn't support the ETH/BTC pair
|
||||
//bloFinSocketClient.FuturesApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BloFin", data.Data.LastPrice)),
|
||||
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
|
||||
coinExSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync(["ETHBTC"], data => UpdateData("CoinEx", data.Data.First().LastPrice)),
|
||||
coinWSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CoinW", data.Data.LastPrice)),
|
||||
coinbaseSocketClient.AdvancedTradeApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Coinbase", data.Data.LastPrice ?? 0)),
|
||||
cryptocomSocketClient.ExchangeApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CryptoCom", data.Data.LastPrice ?? 0)),
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
@using BingX.Net.Interfaces
|
||||
@using Bitfinex.Net.Interfaces
|
||||
@using Bitget.Net.Interfaces;
|
||||
@using BitMart.Net.Interfaces;
|
||||
@using BitMEX.Net.Interfaces;
|
||||
@using Bitstamp.Net.Interfaces;
|
||||
@using Bitunix.Net.Interfaces;
|
||||
@using BloFin.Net.Interfaces;
|
||||
@using Bybit.Net.Interfaces
|
||||
@using CoinEx.Net.Interfaces
|
||||
@using CoinW.Net.Interfaces
|
||||
@using Coinbase.Net.Interfaces
|
||||
@using CryptoExchange.Net.Authentication
|
||||
@@ -39,12 +38,11 @@
|
||||
@inject IBingXOrderBookFactory bingXFactory
|
||||
@inject IBitfinexOrderBookFactory bitfinexFactory
|
||||
@inject IBitgetOrderBookFactory bitgetFactory
|
||||
@inject IBitMartOrderBookFactory bitmartFactory
|
||||
@inject IBitMEXOrderBookFactory bitmexFactory
|
||||
@inject IBitstampOrderBookFactory bitstampFactory
|
||||
@inject IBitunixOrderBookFactory bitunixFactory
|
||||
@inject IBloFinOrderBookFactory bloFinFactory
|
||||
@inject IBybitOrderBookFactory bybitFactory
|
||||
@inject ICoinbaseOrderBookFactory coinbaseFactory
|
||||
@inject ICoinExOrderBookFactory coinExFactory
|
||||
@inject ICoinWOrderBookFactory coinWFactory
|
||||
@inject ICryptoComOrderBookFactory cryptocomFactory
|
||||
@inject IDeepCoinOrderBookFactory deepCoinFactory
|
||||
@@ -102,12 +100,11 @@
|
||||
{ "BingX", bingXFactory.CreateSpot("ETH-BTC") },
|
||||
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") },
|
||||
{ "Bitget", bitgetFactory.CreateSpot("ETHBTC") },
|
||||
{ "BitMart", bitmartFactory.CreateSpot("ETH_BTC", null) },
|
||||
{ "BitMEX", bitmexFactory.Create("ETH_XBT") },
|
||||
{ "Bitstamp", bitstampFactory.Create("ETHUSDT", null) },
|
||||
{ "Bitunix", bitunixFactory.CreateFutures("ETHUSDT", null) },
|
||||
{ "BloFin", bloFinFactory.CreateFutures("ETH-USDT") },
|
||||
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
|
||||
{ "Coinbase", coinbaseFactory.Create("ETH-BTC", null) },
|
||||
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
|
||||
{ "CoinW", coinWFactory.CreateSpot("ETH_BTC") },
|
||||
{ "CryptoCom", cryptocomFactory.Create("ETH_BTC") },
|
||||
{ "GateIo", gateioFactory.CreateSpot("ETH_BTC") },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@page "/SpotClient"
|
||||
@using CryptoExchange.Net.SharedApis
|
||||
@using System.Diagnostics
|
||||
@inject IEnumerable<ISpotTickerRestClient> restClients
|
||||
@inject IEnumerable<IGetTicker> restClients
|
||||
|
||||
<h3>ETH-BTC prices:</h3>
|
||||
@foreach(var price in _prices.OrderBy(p => p.Key))
|
||||
@@ -15,7 +15,7 @@
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "BTC");
|
||||
var tasks = restClients.Select(x => x.GetSpotTickerAsync(new GetTickerRequest(symbol)));
|
||||
var tasks = restClients.Where(x => x.SupportedTradingModes.Contains(TradingMode.Spot)).Select(x => x.GetTickerAsync(new GetTickerRequest(symbol)));
|
||||
await foreach (var ticker in ParallelEnumerateAsync(tasks))
|
||||
{
|
||||
if (ticker.Success)
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
@using BingX.Net.Interfaces
|
||||
@using Bitfinex.Net.Interfaces
|
||||
@using Bitget.Net.Interfaces;
|
||||
@using BitMEX.Net.Interfaces;
|
||||
@using BitMart.Net.Interfaces;
|
||||
@using Bitstamp.Net.Interfaces;
|
||||
@using Bitunix.Net.Interfaces;
|
||||
@using BloFin.Net.Interfaces
|
||||
@using Bybit.Net.Interfaces
|
||||
@using CoinEx.Net.Interfaces
|
||||
@using CoinW.Net.Interfaces
|
||||
@using Coinbase.Net.Interfaces
|
||||
@using CryptoExchange.Net.Interfaces
|
||||
@@ -39,12 +38,11 @@
|
||||
@inject IBingXTrackerFactory bingXFactory
|
||||
@inject IBitfinexTrackerFactory bitfinexFactory
|
||||
@inject IBitgetTrackerFactory bitgetFactory
|
||||
@inject IBitMartTrackerFactory bitmartFactory
|
||||
@inject IBitMEXTrackerFactory bitmexFactory
|
||||
@inject IBitstampTrackerFactory bitstampFactory
|
||||
@inject IBitunixTrackerFactory bitunixFactory
|
||||
@inject IBloFinTrackerFactory bloFinFactory
|
||||
@inject IBybitTrackerFactory bybitFactory
|
||||
@inject ICoinbaseTrackerFactory coinbaseFactory
|
||||
@inject ICoinExTrackerFactory coinExFactory
|
||||
@inject ICoinWTrackerFactory coinWFactory
|
||||
@inject ICryptoComTrackerFactory cryptocomFactory
|
||||
@inject IDeepCoinTrackerFactory deepCoinFactory
|
||||
@@ -95,12 +93,10 @@
|
||||
{ bingXFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitfinexFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitgetFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitmartFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitmexFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitstampFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bloFinFactory.CreateTradeTracker(futuresSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bybitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinbaseFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinExFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinWFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ cryptocomFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ deepCoinFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
|
||||
@@ -38,12 +38,11 @@ namespace BlazorClient
|
||||
services.AddBingX();
|
||||
services.AddBitfinex();
|
||||
services.AddBitget();
|
||||
services.AddBitMart();
|
||||
services.AddBitMEX();
|
||||
services.AddBitstamp();
|
||||
services.AddBitunix();
|
||||
services.AddBloFin();
|
||||
services.AddBybit();
|
||||
services.AddCoinbase();
|
||||
services.AddCoinEx();
|
||||
services.AddCoinW();
|
||||
services.AddCryptoCom();
|
||||
services.AddDeepCoin();
|
||||
|
||||
@@ -13,12 +13,9 @@
|
||||
@using BingX.Net.Interfaces.Clients;
|
||||
@using Bitfinex.Net.Interfaces.Clients;
|
||||
@using Bitget.Net.Interfaces.Clients;
|
||||
@using BitMart.Net.Interfaces.Clients;
|
||||
@using BitMEX.Net.Interfaces.Clients;
|
||||
@using BloFin.Net.Interfaces.Clients;
|
||||
@using Bybit.Net.Interfaces.Clients;
|
||||
@using Coinbase.Net.Interfaces.Clients;
|
||||
@using CoinEx.Net.Interfaces.Clients;
|
||||
@using CoinW.Net.Interfaces.Clients;
|
||||
@using CryptoCom.Net.Interfaces.Clients;
|
||||
@using DeepCoin.Net.Interfaces.Clients;
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="12.13.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="6.13.0" />
|
||||
<PackageReference Include="Binance.Net" Version="13.6.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="7.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -25,18 +25,22 @@ var exchanges = new Dictionary<string, ExchangeClients>(StringComparer.OrdinalIg
|
||||
{
|
||||
["binance"] = new ExchangeClients(
|
||||
"Binance",
|
||||
binanceRest.SpotApi.SharedClient,
|
||||
binanceRest.SpotApi.SharedClient,
|
||||
binanceRest.SpotApi.SharedClient,
|
||||
binanceRest.SpotApi.SharedClient,
|
||||
binanceSocket.SpotApi.SharedClient),
|
||||
binanceRest.SpotApi.SharedApi,
|
||||
binanceRest.SpotApi.SharedApi,
|
||||
binanceRest.SpotApi.SharedApi,
|
||||
binanceRest.SpotApi.SharedApi,
|
||||
binanceRest.SpotApi.SharedApi,
|
||||
binanceRest.SpotApi.SharedApi,
|
||||
binanceSocket.SpotApi.SharedApi),
|
||||
["bybit"] = new ExchangeClients(
|
||||
"Bybit",
|
||||
bybitRest.V5Api.SharedClient,
|
||||
bybitRest.V5Api.SharedClient,
|
||||
bybitRest.V5Api.SharedClient,
|
||||
bybitRest.V5Api.SharedClient,
|
||||
bybitSocket.V5SpotApi.SharedClient)
|
||||
bybitRest.V5Api.SharedApi,
|
||||
bybitRest.V5Api.SharedApi,
|
||||
bybitRest.V5Api.SharedApi,
|
||||
bybitRest.V5Api.SharedApi,
|
||||
bybitRest.V5Api.SharedApi,
|
||||
bybitRest.V5Api.SharedApi,
|
||||
bybitSocket.V5SpotApi.SharedApi)
|
||||
};
|
||||
|
||||
PrintHelp();
|
||||
@@ -98,7 +102,7 @@ async Task PrintPricesAsync(string[] args)
|
||||
var symbol = GetSymbol(args, 1);
|
||||
var tasks = exchanges.Values.Select(async exchange =>
|
||||
{
|
||||
var result = await exchange.TickerRest.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||
var result = await exchange.TickerRest.GetTickerAsync(new GetTickerRequest(symbol));
|
||||
if (!result.Success)
|
||||
{
|
||||
Console.WriteLine($"{exchange.Name,-8} error: {result.Error}");
|
||||
@@ -171,7 +175,7 @@ async Task PrintOpenOrdersAsync(string[] args)
|
||||
var exchange = GetExchange(args, 1);
|
||||
var symbol = GetSymbol(args, 2);
|
||||
|
||||
var result = await exchange.OrderRest.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(symbol));
|
||||
var result = await exchange.GetOpenOrdersRest.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(symbol));
|
||||
if (!result.Success)
|
||||
{
|
||||
Console.WriteLine($"Open orders request failed: {result.Error}");
|
||||
@@ -203,23 +207,9 @@ async Task PlaceLimitOrderAsync(string[] args)
|
||||
SharedQuantity.Base(quantity),
|
||||
price,
|
||||
SharedTimeInForce.GoodTillCanceled,
|
||||
exchange.OrderRest.GenerateClientOrderId());
|
||||
exchange.PlaceOrderRest.GenerateClientOrderId());
|
||||
|
||||
var validationError = exchange.OrderRest.PlaceSpotOrderOptions.ValidateRequest(
|
||||
exchange.Name,
|
||||
request,
|
||||
TradingMode.Spot,
|
||||
exchange.OrderRest.SupportedTradingModes,
|
||||
exchange.OrderRest.SpotSupportedOrderTypes,
|
||||
exchange.OrderRest.SpotSupportedTimeInForce,
|
||||
exchange.OrderRest.SpotSupportedOrderQuantity);
|
||||
if (validationError != null)
|
||||
{
|
||||
Console.WriteLine($"Order request is not valid for {exchange.Name}: {validationError}");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await exchange.OrderRest.PlaceSpotOrderAsync(request);
|
||||
var result = await exchange.PlaceOrderRest.PlaceSpotOrderAsync(request);
|
||||
Console.WriteLine(result.Success
|
||||
? $"Order placed. Id: {result.Data.Id}"
|
||||
: $"Order failed: {result.Error}");
|
||||
@@ -233,7 +223,7 @@ async Task CancelOrderAsync(string[] args)
|
||||
if (string.IsNullOrWhiteSpace(orderId))
|
||||
throw new ArgumentException("Missing order id. Example: cancel-order binance BTC USDT 123456");
|
||||
|
||||
var result = await exchange.OrderRest.CancelSpotOrderAsync(new CancelOrderRequest(symbol, orderId));
|
||||
var result = await exchange.CancelOrderRest.CancelSpotOrderAsync(new CancelOrderRequest(symbol, orderId));
|
||||
Console.WriteLine(result.Success
|
||||
? $"Order canceled. Id: {result.Data.Id}"
|
||||
: $"Cancel failed: {result.Error}");
|
||||
@@ -296,8 +286,10 @@ static void PrintHelp()
|
||||
|
||||
internal record ExchangeClients(
|
||||
string Name,
|
||||
ISpotTickerRestClient TickerRest,
|
||||
IOrderBookRestClient OrderBookRest,
|
||||
IBalanceRestClient BalanceRest,
|
||||
ISpotOrderRestClient OrderRest,
|
||||
ITickerSocketClient TickerSocket);
|
||||
IGetTickerRest TickerRest,
|
||||
IGetOrderBookRest OrderBookRest,
|
||||
IGetBalancesRest BalanceRest,
|
||||
IPlaceSpotOrderRest PlaceOrderRest,
|
||||
IGetOpenSpotOrdersRest GetOpenOrdersRest,
|
||||
ICancelSpotOrderRest CancelOrderRest,
|
||||
ISubscribeTickerSocket TickerSocket);
|
||||
|
||||
@@ -5,13 +5,13 @@ using CryptoExchange.Net.SharedApis;
|
||||
using OKX.Net.Clients;
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
var binanceSpotRestClient = new BinanceRestClient().SpotApi.SharedClient;
|
||||
var okxSpotRestClient = new OKXRestClient().UnifiedApi.SharedClient;
|
||||
var bitmartSpotRestClient = new BitMartRestClient().SpotApi.SharedClient;
|
||||
var binanceSpotRestClient = new BinanceRestClient().SpotApi.SharedApi;
|
||||
var okxSpotRestClient = new OKXRestClient().UnifiedApi.SharedApi;
|
||||
var bitmartSpotRestClient = new BitMartRestClient().SpotApi.SharedApi;
|
||||
|
||||
var binanceSpotSocketClient = new BinanceSocketClient().SpotApi.SharedClient;
|
||||
var okxSpotSocketClient = new OKXSocketClient().UnifiedApi.SharedClient;
|
||||
var bitmartSpotSocketClient = new BitMartSocketClient().SpotApi.SharedClient;
|
||||
var binanceSpotSocketClient = new BinanceSocketClient().SpotApi.SharedApi;
|
||||
var okxSpotSocketClient = new OKXSocketClient().UnifiedApi.SharedApi;
|
||||
var bitmartSpotSocketClient = new BitMartSocketClient().SpotApi.SharedApi;
|
||||
|
||||
await GetLastTradePriceAsync(binanceSpotRestClient, symbol);
|
||||
await GetLastTradePriceAsync(okxSpotRestClient, symbol);
|
||||
@@ -31,9 +31,9 @@ Console.ReadLine();
|
||||
foreach (var subscription in subscriptions)
|
||||
await subscription.CloseAsync();
|
||||
|
||||
async Task GetLastTradePriceAsync(ISpotTickerRestClient client, SharedSymbol symbol)
|
||||
async Task GetLastTradePriceAsync(IGetTickerRest client, SharedSymbol symbol)
|
||||
{
|
||||
var result = await client.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||
var result = await client.GetTickerAsync(new GetTickerRequest(symbol));
|
||||
if (!result.Success)
|
||||
{
|
||||
Console.WriteLine($"Failed to get ticker: {result.Error}");
|
||||
@@ -43,7 +43,7 @@ async Task GetLastTradePriceAsync(ISpotTickerRestClient client, SharedSymbol sym
|
||||
Console.WriteLine($"{client.Exchange} {result.Data.Symbol}: {result.Data.LastPrice}");
|
||||
}
|
||||
|
||||
async Task SubscribeTickerUpdatesAsync(ITickerSocketClient client, SharedSymbol symbol, ICollection<UpdateSubscription> subscriptions)
|
||||
async Task SubscribeTickerUpdatesAsync(ISubscribeTickerSocket client, SharedSymbol symbol, ICollection<UpdateSubscription> subscriptions)
|
||||
{
|
||||
var result = await client.SubscribeToTickerUpdatesAsync(new SubscribeTickerRequest(symbol), update =>
|
||||
{
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="12.13.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="3.11.2" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="4.14.0" />
|
||||
<PackageReference Include="Binance.Net" Version="13.6.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="4.5.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="5.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -23,9 +23,9 @@ For single-exchange code, see also the AI files in each exchange's repository (B
|
||||
See [cryptoexchange-skills-hub](https://github.com/JKorf/cryptoexchange-skills-hub) for installable skills.
|
||||
|
||||
**Quick prompt to verify your assistant is using these:**
|
||||
> "Show me how to fetch BTC/USDT spot tickers from Binance and OKX concurrently in C# using the SharedApis pattern."
|
||||
> "Show me how to fetch BTC/USDT spot tickers from Binance and OKX concurrently in C# using the SharedApis V2 pattern."
|
||||
|
||||
The expected output should use `.SharedClient` properties, `SharedSymbol`, `ISpotTickerRestClient`, and `Task.WhenAll`.
|
||||
The expected output should use `.SharedApi` properties, `SharedSymbol`, `IGetTickerRest`, and `Task.WhenAll`.
|
||||
|
||||
### CryptoExchange.Net Ecosystem
|
||||
Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider using a referral link to support development, as well as potentially get some trading fee discount!
|
||||
@@ -38,8 +38,8 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|
||||
||Bitfinex|CEX|[JKorf/Bitfinex.Net](https://github.com/JKorf/Bitfinex.Net)|[](https://www.nuget.org/packages/Bitfinex.Net)|-|-|
|
||||
||Bitget|CEX|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[](https://www.nuget.org/packages/JK.Bitget.Net)|[Link](https://partner.bitget.com/bg/1qlf6pj1)|20%|
|
||||
||BitMart|CEX|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[](https://www.nuget.org/packages/BitMart.Net)|[Link](https://www.bitmart.com/invite/JKorfAPI/en-US)|30%|
|
||||
||BitMEX|CEX|[JKorf/BitMEX.Net](https://github.com/JKorf/BitMEX.Net)|[](https://www.nuget.org/packages/JKorf.BitMEX.Net)|[Link](https://www.bitmex.com/app/register/94f98e)|30%|
|
||||
||Bitstamp|CEX|[JKorf/Bitstamp.Net](https://github.com/JKorf/Bitstamp.Net)|[](https://www.nuget.org/packages/Bitstamp.Net)|-|-|
|
||||
||Bitunix|CEX|[JKorf/Bitunix.Net](https://github.com/JKorf/Bitunix.Net)|[](https://www.nuget.org/packages/Bitunix.Net)|-|-|
|
||||
||BloFin|CEX|[JKorf/BloFin.Net](https://github.com/JKorf/BloFin.Net)|[](https://www.nuget.org/packages/BloFin.Net)|-|-|
|
||||
||Bybit|CEX|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[](https://www.nuget.org/packages/Bybit.Net)|[Link](https://partner.bybit.com/b/jkorf)|-|
|
||||
||Coinbase|CEX|[JKorf/Coinbase.Net](https://github.com/JKorf/Coinbase.Net)|[](https://www.nuget.org/packages/JKorf.Coinbase.Net)|[Link](https://advanced.coinbase.com/join/T6H54H8)|-|
|
||||
@@ -128,6 +128,68 @@ Various:
|
||||
* PlatformInfo now required support environment names in the constructor
|
||||
|
||||
## Release notes
|
||||
* Version 13.2.3 - 03 Nov 2026
|
||||
* Added additional constructor for NoApiCredentialsError
|
||||
* Updated test error messages
|
||||
* Fixed some timing issues in WebSocket closing
|
||||
|
||||
* Version 13.2.2 - 22 Oct 2026
|
||||
* Fixed UserDataTracker config not getting set correctly
|
||||
* Added UserDataTracker warning for missing tracked symbols if open orders request requires it
|
||||
|
||||
* Version 13.2.1 - 19 Oct 2026
|
||||
* Fixed subscription without any predefined routes failing to subscribe
|
||||
* Fixed test validation failing with empty body string
|
||||
|
||||
* Version 13.2.0 - 13 Oct 2026
|
||||
* Added Shared API ISubscribeFundingInfoSocket
|
||||
* Added DelayAfterConnect to SocketApiOptions
|
||||
* Added processor parameter for some methods in SocketApiClient to allow specific overriding behavior
|
||||
* Updated WebSocket duplicate subscription handling
|
||||
* Updated Shared APIs ISubscribeIncrementalOrderBook
|
||||
* Fixed incorrect status keeping of WebSocket subscriptions which don't expect a response
|
||||
* Fixed DelayAfterConnect not being respected on reconnection
|
||||
|
||||
* Version 13.1.0 - 28 Sep 2026
|
||||
* Added UnsubscribeAllAsync method to Shared APIs ISharedSubscription and ISharedApiClientBase
|
||||
* Added WaitAllAsync extension method for waiting on an IAsyncEnumerable to finish
|
||||
* Added SubscribeAllAsync extension methods for Shared API resolved capabilities
|
||||
|
||||
* Version 13.0.0 - 23 Sep 2026
|
||||
* Shared APIs
|
||||
* Added Shared API V2 with fine-grained capability interfaces for individual REST requests, WebSocket requests and subscriptions
|
||||
* Added transport-agnostic capability interfaces with REST- and WebSocket-specific variants where applicable
|
||||
* Added dynamic capability resolution support with optional transport preference option
|
||||
* Added `RequestParameterRules` and `ExchangeParameterRules` properties on Shared API options making request parameter rules and support clearer
|
||||
* Added new Shared API capabilities for ledger history, funding information and history, leverage tiers, transfers, mark prices and index prices
|
||||
* Added new capabilities for placing and editing multiple spot and futures orders
|
||||
* Added new capabilities for cancelling all open orders and all open symbol orders
|
||||
* Added new incremental order book, mark price and index price subscriptions
|
||||
* Added ReduceOnly parameter to PlaceFuturesTriggerOrderRequest
|
||||
* Added new `SharedTicker` as the common V2 ticker model for spot and futures markets
|
||||
* For additional futures ticker info like mark/index price and funding info separate interfaces are available in V2
|
||||
* Added `SharedSpotOrderUpdate` and `SharedFuturesOrderUpdate` models for WebSocket order updates
|
||||
* Added `IExchangeCallResult` and `IExchangeCallResult<T>` for transport-agnostic Shared API results
|
||||
* Retained the V1 aggregate Shared API interfaces for backwards compatibility
|
||||
* Renamed GetAssetsOptions to GetAllAssetsOptions
|
||||
* Renamed GetWithdrawalsOptions to GetWithdrawalHistoryOptions
|
||||
* Renamed GetDepositsOptions to GetDepositHistoryOptions
|
||||
* GetFuturesTickerOptions and GetSpotTickerOptions have been replaced by GetTickerOptions
|
||||
* Deprecated Subscribe request constructors using `params` for `SharedSymbol` parameter
|
||||
* Rate limiting
|
||||
* Added `RateLimitAdmission` for restricting requests to a configurable maximum rate limit utilization
|
||||
* Added the `RateLimitAdmission` client option callback for assigning admission rules based on request definition and weight
|
||||
* Added `WithRateLimitAdmissionAsync` for applying a rate limit admission rule to a specific REST, WebSocket or Shared API operation
|
||||
* Added configurable safety margins to fixed, sliding and fixed-after-first rate limit windows
|
||||
* Fixed decay rate limiter calculations and handling of partial decay progress
|
||||
* Fixed fixed and sliding window boundary calculations
|
||||
* Added automatic in-flight request coalescing for identical public REST GET requests
|
||||
* Sending identical public GET requests on the same client will only send a single request to the server and use the same response
|
||||
* Coalescing is enabled by default and can be disabled with the `RequestCoalescingEnabled` client option
|
||||
* Added `DataTime`, `DataTimeLocal` and `SequenceNumber` propagation when converting `DataEvent<T>` instances
|
||||
* Fixed form data URL encoding on .NET Framework when parameter values contain special characters
|
||||
* Fixed array converter not correctly handling decimal parsing for certain notations
|
||||
|
||||
* Version 12.5.1 - 01 Sep 2026
|
||||
* Fixed KlineTracker reporting incorrect High/Low price on GetStats result
|
||||
* Fixed caching issue for auth requests
|
||||
|
||||
@@ -35,7 +35,7 @@ var placementCapabilityResolution = sharedClient.GetCapability(
|
||||
|
||||
if (placementCapabilityResolution is not null)
|
||||
{
|
||||
IPlaceFuturesOrderRest api = placement.Capability;
|
||||
IPlaceFuturesOrderRest api = placementCapabilityResolution.Capability;
|
||||
// placement.Options describes parameter and trading-mode support for this implementation.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
# CryptoExchange.Net Shared API V2 map
|
||||
|
||||
This is a compact map for choosing Shared API V2 types. All listed types are in `CryptoExchange.Net.SharedApis`. Exchange libraries implement subsets; the map describes available abstractions, not guaranteed exchange support.
|
||||
|
||||
## Entry points
|
||||
|
||||
| Need | Use |
|
||||
| --- | --- |
|
||||
| Known exchange and API surface | Its typed `.SharedApi` property |
|
||||
| Runtime selection within one exchange | Exchange-wide `I[Exchange]SharedApiClient` |
|
||||
| Strongly typed runtime lookup key | `SharedCapabilities` |
|
||||
| One preferred match | `GetCapability(...)` |
|
||||
| Every matching surface/transport in one exchange | `GetCapabilities(...)` |
|
||||
| V1 compatibility during migration | `.SharedClient` |
|
||||
|
||||
Prefer a direct typed `.SharedApi` when the API surface is known. Dynamic lookup returns `SharedCapabilityResolution<T>?`; handle `null`.
|
||||
|
||||
```csharp
|
||||
IGetTickerRest ticker = restClient.SpotApi.SharedApi;
|
||||
var result = await ticker.GetTickerAsync(request);
|
||||
```
|
||||
|
||||
```csharp
|
||||
var match = sharedClient.GetCapability(
|
||||
SharedCapabilities.Tickers.GetTicker.Rest,
|
||||
TradingMode.Spot);
|
||||
```
|
||||
|
||||
## Interface suffixes and results
|
||||
|
||||
| Interface shape | Transport | Typical result |
|
||||
| --- | --- | --- |
|
||||
| Operation with no suffix, e.g. `IPlaceSpotOrder` | selected/preferred | `IExchangeCallResult<T>` |
|
||||
| `...Rest` | REST | `HttpResult<T>` |
|
||||
| socket command, e.g. `IPlaceSpotOrderSocket` | WebSocket | `QueryResult<T>` |
|
||||
| subscription, e.g. `ISubscribeTickerSocket` | WebSocket | `WebSocketResult<UpdateSubscription>` |
|
||||
|
||||
When a `SharedCapabilities` entry supports multiple transports, use the base entry for preferred transport, `.Rest` for REST, or `.Socket` for socket. Exchange-wide preferred transport normally defaults to REST.
|
||||
|
||||
## Market data
|
||||
|
||||
| Operation | Capability interface | Lookup reference |
|
||||
| --- | --- | --- |
|
||||
| Get one ticker | `IGetTickerRest` | `SharedCapabilities.Tickers.GetTicker.Rest` |
|
||||
| Get all tickers | `IGetAllTickersRest` | `SharedCapabilities.Tickers.GetAllTickers.Rest` |
|
||||
| Subscribe to one ticker | `ISubscribeTickerSocket` | `SharedCapabilities.Tickers.SubscribeTicker` |
|
||||
| Subscribe to all tickers | `ISubscribeAllTickersSocket` | `SharedCapabilities.Tickers.SubscribeAllTickers` |
|
||||
| Subscribe to book ticker | `ISubscribeBookTickerSocket` | `SharedCapabilities.Tickers.SubscribeBookTicker` |
|
||||
| Get order book | `IGetOrderBookRest` | `SharedCapabilities.OrderBooks.GetOrderBook.Rest` |
|
||||
| Get book ticker | `IGetBookTickerRest` | `SharedCapabilities.OrderBooks.GetBookTicker.Rest` |
|
||||
| Subscribe to order book | `ISubscribeOrderBookSocket` | `SharedCapabilities.OrderBooks.SubscribeOrderBook` |
|
||||
| Subscribe to incremental book | `ISubscribeIncrementalOrderBookSocket` | `SharedCapabilities.OrderBooks.SubscribeIncrementalOrderBook` |
|
||||
| Get klines | `IGetKlinesRest` | `SharedCapabilities.Klines.GetKlines.Rest` |
|
||||
| Subscribe to klines | `ISubscribeKlinesSocket` | `SharedCapabilities.Klines.SubscribeKlines` |
|
||||
| Get recent trades | `IGetRecentTradesRest` | `SharedCapabilities.Trades.GetRecentTrades.Rest` |
|
||||
| Subscribe to trades | `ISubscribeTradesSocket` | `SharedCapabilities.Trades.SubscribeTrades` |
|
||||
| Get spot symbols | `IGetSpotSymbolsRest` | `SharedCapabilities.Symbols.GetSpotSymbols.Rest` |
|
||||
| Get futures symbols | `IGetFuturesSymbolsRest` | `SharedCapabilities.Symbols.GetFuturesSymbols.Rest` |
|
||||
|
||||
Ticker V2 methods are `GetTickerAsync` and `GetAllTickersAsync`; both use `SharedTicker` for spot and futures. Use separate mark-price, index-price, funding, or open-interest capabilities for derivatives-specific data.
|
||||
|
||||
## Spot orders
|
||||
|
||||
| Operation | Capability family | Lookup reference |
|
||||
| --- | --- | --- |
|
||||
| Place | `IPlaceSpotOrder`, `IPlaceSpotOrderRest`, `IPlaceSpotOrderSocket` | `SharedCapabilities.Orders.Spot.PlaceOrder[.Rest/.Socket]` |
|
||||
| Edit | `IEditSpotOrder`, `IEditSpotOrderRest`, `IEditSpotOrderSocket` | `SharedCapabilities.Orders.Spot.EditOrder[.Rest/.Socket]` |
|
||||
| Cancel | `ICancelSpotOrder`, `ICancelSpotOrderRest`, `ICancelSpotOrderSocket` | `SharedCapabilities.Orders.Spot.CancelOrder[.Rest/.Socket]` |
|
||||
| Get one | `IGetSpotOrderRest` | `SharedCapabilities.Orders.Spot.GetOrder.Rest` |
|
||||
| Get open | `IGetOpenSpotOrdersRest` | `SharedCapabilities.Orders.Spot.GetOpenOrders.Rest` |
|
||||
| Get closed | `IGetClosedSpotOrdersRest` | `SharedCapabilities.Orders.Spot.GetClosedOrders.Rest` |
|
||||
| Subscribe to updates | `ISubscribeSpotOrdersSocket` | `SharedCapabilities.Orders.Spot.SubscribeOrders` |
|
||||
|
||||
Client-order-id variants exist for edit, cancel, and get. Batch placement and cancel-all capabilities also exist; resolve them only when the application needs those operations and the exchange supports them.
|
||||
|
||||
## Futures orders
|
||||
|
||||
| Operation | Capability family | Lookup reference |
|
||||
| --- | --- | --- |
|
||||
| Place | `IPlaceFuturesOrder`, `IPlaceFuturesOrderRest`, `IPlaceFuturesOrderSocket` | `SharedCapabilities.Orders.Futures.PlaceOrder[.Rest/.Socket]` |
|
||||
| Edit | `IEditFuturesOrder`, `IEditFuturesOrderRest`, `IEditFuturesOrderSocket` | `SharedCapabilities.Orders.Futures.EditOrder[.Rest/.Socket]` |
|
||||
| Cancel | `ICancelFuturesOrder`, `ICancelFuturesOrderRest`, `ICancelFuturesOrderSocket` | `SharedCapabilities.Orders.Futures.CancelOrder[.Rest/.Socket]` |
|
||||
| Get one | `IGetFuturesOrderRest` | `SharedCapabilities.Orders.Futures.GetOrder.Rest` |
|
||||
| Get open | `IGetOpenFuturesOrdersRest` | `SharedCapabilities.Orders.Futures.GetOpenOrders.Rest` |
|
||||
| Get closed | `IGetClosedFuturesOrdersRest` | `SharedCapabilities.Orders.Futures.GetClosedOrders.Rest` |
|
||||
| Subscribe to updates | `ISubscribeFuturesOrdersSocket` | `SharedCapabilities.Orders.Futures.SubscribeOrders` |
|
||||
|
||||
Specify `TradingMode.PerpetualLinear`, `PerpetualInverse`, or the appropriate delivery mode when multiple futures APIs can match.
|
||||
|
||||
## Account and derivatives
|
||||
|
||||
Use the following capability families for common account workflows:
|
||||
|
||||
| Area | Common interfaces |
|
||||
| --- | --- |
|
||||
| Balances | `IGetBalancesRest`, `ISubscribeBalancesSocket` |
|
||||
| Positions | `IGetPositionsRest`, `ISubscribePositionsSocket`, `IGetPositionHistoryRest` |
|
||||
| User trades | `IGetSpotUserTradeHistoryRest`, `IGetFuturesUserTradeHistoryRest`, `ISubscribeUserTradesSocket` |
|
||||
| Fees | `IGetFeesRest` |
|
||||
| Deposits | `IGetDepositAddressesRest`, `IGetDepositHistoryRest` |
|
||||
| Withdrawals | `IWithdrawRest`, `IGetWithdrawalHistoryRest` |
|
||||
| Transfers | `ITransferRest`, `IGetTransferHistoryRest` |
|
||||
| Leverage | `IGetLeverageRest`, `ISetLeverageRest`, `IGetLeverageTiersRest` |
|
||||
| Funding | `IGetFundingInfoRest`, `IGetFundingRateHistoryRest`, `IGetUserFundingHistoryRest` |
|
||||
| Mark/index data | `IGetMarkPriceRest`, `IGetIndexPriceRest` and their all-market/subscription variants |
|
||||
| Open interest | `IGetOpenInterestRest` |
|
||||
|
||||
The `SharedCapabilities` catalog groups these under matching plural categories such as `Balances`, `Positions`, `Funding`, `Leverage`, `MarkPrices`, and `IndexPrices`.
|
||||
|
||||
## Parameter support map
|
||||
|
||||
Every successful runtime resolution includes `Options`:
|
||||
|
||||
| Property | Meaning |
|
||||
| --- | --- |
|
||||
| `RequestParameterRules` | Shared request fields that are required, optional, or unsupported |
|
||||
| `ExchangeParameterRules` | Exchange-specific fields accepted through `ExchangeParameters` |
|
||||
| `SupportedTradingModes` | Modes supported by this capability implementation |
|
||||
| `NeedsAuthentication` | Whether credentials are required |
|
||||
|
||||
Capability presence is not proof that all properties on its request model are accepted. Inspect rules for dynamically generated requests.
|
||||
|
||||
## Common V1 to V2 mappings
|
||||
|
||||
| V1 call | V2 capability call |
|
||||
| --- | --- |
|
||||
| `ISpotTickerRestClient.GetSpotTickerAsync` | `IGetTickerRest.GetTickerAsync` |
|
||||
| `ISpotTickerRestClient.GetSpotTickersAsync` | `IGetAllTickersRest.GetAllTickersAsync` |
|
||||
| `ISpotOrderRestClient.PlaceSpotOrderAsync` | `IPlaceSpotOrderRest.PlaceSpotOrderAsync` |
|
||||
| `ISpotOrderRestClient.CancelSpotOrderAsync` | `ICancelSpotOrderRest.CancelSpotOrderAsync` |
|
||||
| `IFuturesOrderRestClient.PlaceFuturesOrderAsync` | `IPlaceFuturesOrderRest.PlaceFuturesOrderAsync` |
|
||||
| `IFuturesOrderRestClient.CancelFuturesOrderAsync` | `ICancelFuturesOrderRest.CancelFuturesOrderAsync` |
|
||||
| `IFuturesOrderRestClient.GetPositionsAsync` | `IGetPositionsRest.GetPositionsAsync` |
|
||||
|
||||
V1 uses `.SharedClient`; V2 uses `.SharedApi`. Migrate operation by operation.
|
||||
|
||||
## Selection checklist
|
||||
|
||||
1. Use the exchange-native API if portability is unnecessary.
|
||||
2. For portable code, identify the single operation capability required.
|
||||
3. Use the typed `.SharedApi` directly when the surface is known.
|
||||
4. Otherwise resolve through the exchange-wide shared client and handle `null`.
|
||||
5. Specify trading mode and transport when ambiguity matters.
|
||||
6. Inspect parameter rules for dynamic requests.
|
||||
7. Check `Success` before using `Data`.
|
||||
|
||||
For migration edge cases and CryptoClients.Net cross-exchange lookup, see `SHARED_API_V2_MIGRATION.md`.
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
# CryptoExchange.Net — full AI context
|
||||
|
||||
Library: CryptoExchange.Net
|
||||
Library version: 13.2.2
|
||||
CryptoExchange.Net package version: 13.2.2
|
||||
Language: C#/.NET
|
||||
Targets: netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0
|
||||
|
||||
## Purpose
|
||||
|
||||
CryptoExchange.Net is the base library used by 28+ exchange-specific clients. It provides shared REST and WebSocket infrastructure, authentication and result conventions, rate limiting, order-book support, and exchange-agnostic Shared APIs.
|
||||
|
||||
CryptoExchange.Net does not provide exchange endpoints by itself. Install the exchange libraries required by the application, such as Binance.Net, Bybit.Net, JK.OKX.Net, Kraken.Net, or Coinbase.Net. Install `CryptoClients.Net` when one package should provide the full exchange bundle.
|
||||
|
||||
Use an exchange's native API for code tied to that exchange. Use Shared API V2 for portable code that performs the same operation against multiple exchanges.
|
||||
|
||||
## Shared API V2 mental model
|
||||
|
||||
Shared API V2 models each operation as a fine-grained capability. A typed exchange API surface exposes implemented capabilities through `.SharedApi`. Examples include:
|
||||
|
||||
- `IGetTickerRest`
|
||||
- `IGetOrderBookRest`
|
||||
- `IGetKlinesRest`
|
||||
- `IPlaceSpotOrderRest`
|
||||
- `ICancelFuturesOrderRest`
|
||||
- `ISubscribeTickerSocket`
|
||||
- `ISubscribeTradesSocket`
|
||||
|
||||
This replaces the V1 assumption that one broad interface represents a complete feature group. An exchange may support order placement without editing, or REST placement without socket placement. Write services against only the capabilities they require.
|
||||
|
||||
V1 aggregate interfaces remain available through `.SharedClient` for incremental migration. Use `.SharedApi` for new code.
|
||||
|
||||
## Installation
|
||||
|
||||
Install only the exchange packages needed:
|
||||
|
||||
```bash
|
||||
dotnet add package Binance.Net
|
||||
dotnet add package JK.OKX.Net
|
||||
dotnet add package Bybit.Net
|
||||
```
|
||||
|
||||
Or install the combined package:
|
||||
|
||||
```bash
|
||||
dotnet add package CryptoClients.Net
|
||||
```
|
||||
|
||||
Do not add CryptoExchange.Net alone and expect to create an exchange client.
|
||||
|
||||
## Direct typed capability usage
|
||||
|
||||
When the exchange and API surface are known, assign `.SharedApi` to the operation interface. The API surface's compile-time type exposes only the capabilities implemented there.
|
||||
|
||||
```csharp
|
||||
using Binance.Net.Clients;
|
||||
using OKX.Net.Clients;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
|
||||
IGetTickerRest binance = new BinanceRestClient().SpotApi.SharedApi;
|
||||
IGetTickerRest okx = new OKXRestClient().UnifiedApi.SharedApi;
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
var result = await binance.GetTickerAsync(new GetTickerRequest(symbol));
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
Console.WriteLine($"[{result.Exchange}] {result.Error}");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[{result.Exchange}] {result.Data!.LastPrice}");
|
||||
```
|
||||
|
||||
Use `SharedSymbol` rather than exchange-native strings. Each exchange implementation converts base asset, quote asset, and trading mode into its own symbol format.
|
||||
|
||||
Common trading modes include `Spot`, `PerpetualLinear`, and `PerpetualInverse`. Use the mode that corresponds to the intended exchange API surface.
|
||||
|
||||
## Multi-exchange concurrency
|
||||
|
||||
Shared capabilities allow uniform concurrent calls:
|
||||
|
||||
```csharp
|
||||
var clients = new IGetTickerRest[]
|
||||
{
|
||||
new BinanceRestClient().SpotApi.SharedApi,
|
||||
new OKXRestClient().UnifiedApi.SharedApi,
|
||||
new BybitRestClient().V5Api.SharedApi,
|
||||
};
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
var tasks = clients.Select(client =>
|
||||
client.GetTickerAsync(new GetTickerRequest(symbol)));
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
foreach (var ticker in results.Where(x => x.Success))
|
||||
Console.WriteLine($"{ticker.Exchange}: {ticker.Data!.LastPrice}");
|
||||
```
|
||||
|
||||
Independent requests should normally run concurrently. Reuse long-lived clients or dependency-injected clients instead of creating clients for each request.
|
||||
|
||||
## Dynamic capability resolution
|
||||
|
||||
Each exchange library registers an exchange-wide `I[Exchange]SharedApiClient`. Use this client when the API surface, trading mode, or transport is selected at runtime.
|
||||
|
||||
`SharedCapabilities` is a catalog of strongly typed references. A reference identifies the interface to find; it does not guarantee support and does not perform an exchange request.
|
||||
|
||||
```csharp
|
||||
var resolution = sharedClient.GetCapability(
|
||||
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
|
||||
TradingMode.PerpetualLinear);
|
||||
|
||||
if (resolution is null)
|
||||
{
|
||||
Console.WriteLine("Futures REST placement is unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await resolution.Capability.PlaceFuturesOrderAsync(request);
|
||||
```
|
||||
|
||||
`GetCapability` returns one `SharedCapabilityResolution<T>` or `null`. The resolution exposes:
|
||||
|
||||
- `Capability`: the callable typed interface.
|
||||
- `Options`: capability metadata and request rules.
|
||||
- `Exchange`: the exchange name.
|
||||
- `Transport`: REST or socket.
|
||||
|
||||
`GetCapabilities` returns every matching implementation on that exchange-wide client. Pass a `TradingMode` when an exchange can expose multiple matching API surfaces, especially separate linear and inverse futures APIs.
|
||||
|
||||
Use the typed `.SharedApi` property when the desired surface is already known. Use dynamic resolution only when the choice or support is genuinely runtime-dependent.
|
||||
|
||||
## Transport selection
|
||||
|
||||
Some operations have transport-agnostic, REST-specific, and socket-specific variants. Order placement is representative:
|
||||
|
||||
- `IPlaceSpotOrder`: transport-agnostic, returns `IExchangeCallResult<SharedId>`.
|
||||
- `IPlaceSpotOrderRest`: REST-specific, returns `HttpResult<SharedId>`.
|
||||
- `IPlaceSpotOrderSocket`: socket-specific, returns `QueryResult<SharedId>`.
|
||||
|
||||
Subscription interfaces such as `ISubscribeTickerSocket` return `WebSocketResult<UpdateSubscription>`.
|
||||
|
||||
Use a capability reference without a suffix when either transport is acceptable:
|
||||
|
||||
```csharp
|
||||
var preferred = sharedClient.GetCapability(
|
||||
SharedCapabilities.Orders.Spot.PlaceOrder);
|
||||
```
|
||||
|
||||
Use `.Rest` or `.Socket` when the transport or concrete result type matters:
|
||||
|
||||
```csharp
|
||||
var rest = sharedClient.GetCapability(
|
||||
SharedCapabilities.Orders.Spot.PlaceOrder.Rest);
|
||||
|
||||
var socket = sharedClient.GetCapability(
|
||||
SharedCapabilities.Orders.Spot.PlaceOrder.Socket);
|
||||
```
|
||||
|
||||
Exchange-wide transport-agnostic lookup follows the configured `SharedApi.PreferredTransport`, which normally defaults to REST. Do not rely on that preference when application semantics require a particular transport.
|
||||
|
||||
## Result handling
|
||||
|
||||
All result types expose `Success`, `Data`, and `Error`. Check `Success` before reading `Data`.
|
||||
|
||||
- REST capability: `HttpResult<T>`
|
||||
- socket command capability: `QueryResult<T>`
|
||||
- socket subscription capability: `WebSocketResult<UpdateSubscription>`
|
||||
- transport-agnostic capability: `IExchangeCallResult<T>`
|
||||
|
||||
Shared results carry the exchange name, which should be included in logs and aggregation records.
|
||||
|
||||
```csharp
|
||||
if (!result.Success)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"{Exchange} ticker request failed: {Error}",
|
||||
result.Exchange,
|
||||
result.Error);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
## Request parameter discovery
|
||||
|
||||
A capability can exist even when an exchange does not accept every property on the shared request. Inspect the selected capability's `Options` before building fully dynamic requests:
|
||||
|
||||
- `RequestParameterRules`: each shared request field is `Required`, `Optional`, or `NotSupported`.
|
||||
- `ExchangeParameterRules`: exchange-specific required or optional parameters supplied through the request's `ExchangeParameters`.
|
||||
- `SupportedTradingModes`: modes supported by this particular implementation.
|
||||
|
||||
```csharp
|
||||
var placement = sharedClient.GetCapability(
|
||||
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
|
||||
TradingMode.PerpetualLinear);
|
||||
|
||||
var leverageRule = placement?.Options.RequestParameterRules
|
||||
.FirstOrDefault(rule =>
|
||||
rule.Name == nameof(PlaceFuturesOrderRequest.Leverage));
|
||||
|
||||
if (leverageRule?.Support == RequestParameterSupport.NotSupported)
|
||||
Console.WriteLine("Set leverage with a separate capability.");
|
||||
```
|
||||
|
||||
Do not infer field support from the request model alone. Handle a missing capability, unsupported trading mode, required field, and required exchange parameter before submitting a trading request.
|
||||
|
||||
## Dependency injection
|
||||
|
||||
Each exchange library provides a `services.Add[Exchange](...)` extension. Registration includes native clients, V1 shared facades, V2 operation capabilities, and the exchange-wide shared client.
|
||||
|
||||
Direct capability injection is appropriate when one implementation is intended:
|
||||
|
||||
```csharp
|
||||
public sealed class TickerService
|
||||
{
|
||||
private readonly IGetTickerRest _ticker;
|
||||
|
||||
public TickerService(IGetTickerRest ticker)
|
||||
=> _ticker = ticker;
|
||||
|
||||
public Task<HttpResult<SharedTicker>> GetAsync(
|
||||
SharedSymbol symbol,
|
||||
CancellationToken ct = default)
|
||||
=> _ticker.GetTickerAsync(new GetTickerRequest(symbol), ct);
|
||||
}
|
||||
```
|
||||
|
||||
If multiple exchanges or multiple API surfaces register the same capability interface, inject the exchange-specific shared client and select a typed API property or use `GetCapability`. Resolving one unqualified capability does not express which registered implementation is desired.
|
||||
|
||||
`CryptoClients.Net` registers `IExchangeSharedApiClient` for V2 lookup across exchanges. Its `GetCapabilities` returns one preferred matching implementation per exchange, while `GetImplementations` returns every matching transport and API surface.
|
||||
|
||||
## Common capability families
|
||||
|
||||
Frequently used market-data capabilities:
|
||||
|
||||
- Tickers: `IGetTickerRest`, `IGetAllTickersRest`, `ISubscribeTickerSocket`, `ISubscribeAllTickersSocket`, `ISubscribeBookTickerSocket`.
|
||||
- Order books: `IGetOrderBookRest`, `IGetBookTickerRest`, `ISubscribeOrderBookSocket`, `ISubscribeIncrementalOrderBookSocket`.
|
||||
- Klines and trades: `IGetKlinesRest`, `ISubscribeKlinesSocket`, `IGetRecentTradesRest`, `ISubscribeTradesSocket`.
|
||||
- Symbols: `IGetSpotSymbolsRest`, `IGetFuturesSymbolsRest`.
|
||||
|
||||
Frequently used trading and account capabilities:
|
||||
|
||||
- Spot and futures order placement, editing, cancellation, retrieval, open/closed orders, and order-update subscriptions.
|
||||
- Balances, positions, user trades, fees, deposits, withdrawals, and transfers.
|
||||
- Funding, open interest, leverage, mark price, and index price operations for derivatives.
|
||||
|
||||
Use `docs/ai-api-map.md` or the typed API surface for names. Do not assume every exchange implements every family.
|
||||
|
||||
## V2 ticker behavior
|
||||
|
||||
V2 uses `GetTickerAsync` and `GetAllTickersAsync` for both spot and futures and returns `SharedTicker`. Futures-specific mark price, index price, and funding data have separate capabilities rather than being assumed to exist on the common ticker.
|
||||
|
||||
```csharp
|
||||
IGetTickerRest ticker = restClient.SpotApi.SharedApi;
|
||||
var result = await ticker.GetTickerAsync(
|
||||
new GetTickerRequest(
|
||||
new SharedSymbol(TradingMode.Spot, "ETH", "USDT")));
|
||||
```
|
||||
|
||||
## Order and subscription behavior
|
||||
|
||||
V2 exposes REST and socket order commands under the same operation capability when both are supported. Choose the transport-specific interface when the caller needs `HttpResult<T>` or `QueryResult<T>`.
|
||||
|
||||
Order-update subscriptions use `SharedSpotOrderUpdate` and `SharedFuturesOrderUpdate`. REST order retrieval returns `SharedSpotOrder` or `SharedFuturesOrder`. Keep update-only stream semantics out of ordinary order snapshots.
|
||||
|
||||
`ICloseFullPosition` closes the entire position and has no quantity parameter. It is not a replacement for partial close logic; use a suitable order capability when an exchange supports partial reduction.
|
||||
|
||||
## V1 migration essentials
|
||||
|
||||
V1 remains available through `.SharedClient`; V2 is exposed through `.SharedApi`. Both are views over the exchange API, so migration can happen operation by operation.
|
||||
|
||||
```csharp
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
var request = new GetTickerRequest(symbol);
|
||||
|
||||
// V1 compatibility facade
|
||||
var oldResult = await restClient.SpotApi.SharedClient
|
||||
.GetSpotTickerAsync(request);
|
||||
|
||||
// V2 capability
|
||||
var newResult = await restClient.SpotApi.SharedApi
|
||||
.GetTickerAsync(request);
|
||||
```
|
||||
|
||||
Representative mappings:
|
||||
|
||||
- `ISpotTickerRestClient.GetSpotTickerAsync` -> `IGetTickerRest.GetTickerAsync`
|
||||
- `ISpotOrderRestClient.PlaceSpotOrderAsync` -> `IPlaceSpotOrderRest.PlaceSpotOrderAsync`
|
||||
- `ISpotOrderRestClient.CancelSpotOrderAsync` -> `ICancelSpotOrderRest.CancelSpotOrderAsync`
|
||||
- `IFuturesOrderRestClient.PlaceFuturesOrderAsync` -> `IPlaceFuturesOrderRest.PlaceFuturesOrderAsync`
|
||||
- `IFuturesOrderRestClient.GetPositionsAsync` -> `IGetPositionsRest.GetPositionsAsync`
|
||||
|
||||
Transport-agnostic V2 methods return `IExchangeCallResult<T>`. Select `...Rest` or `...Socket` if old code depends on a concrete transport result. See `docs/SHARED_API_V2_MIGRATION.md` for detailed migration examples and CryptoClients.Net behavior.
|
||||
|
||||
## Code-generation rules
|
||||
|
||||
- Use `.SharedApi` and fine-grained V2 interfaces for new portable code.
|
||||
- Use `.SharedClient` only when maintaining or incrementally migrating V1 code.
|
||||
- Use `SharedSymbol` instead of native symbol strings.
|
||||
- Resolve capability support rather than assuming it.
|
||||
- Inspect capability options when requests are generated dynamically.
|
||||
- Select REST or socket explicitly when transport semantics matter.
|
||||
- Check `Success` before accessing `Data`.
|
||||
- Reuse clients through DI and keep calls asynchronous.
|
||||
- Run independent exchange calls concurrently.
|
||||
- Keep exchange-native models out of cross-exchange service contracts.
|
||||
|
||||
## Implementing an exchange library
|
||||
|
||||
Exchange implementations derive from the shared REST and socket client bases, provide their authentication and option types, and expose typed Shared API classes. Implement the fine-grained capability interfaces actually supported by each API surface and publish accurate `CapabilityOptions`, including trading modes and request/exchange parameter rules. Register the Shared APIs and exchange-wide shared client in the exchange library's DI extension.
|
||||
|
||||
Use established exchange libraries as references. Do not advertise a capability merely because a similar native endpoint exists; its shared request and result semantics must be implemented correctly.
|
||||
|
||||
## References
|
||||
|
||||
- Source: https://github.com/JKorf/CryptoExchange.Net
|
||||
- Documentation: https://cryptoexchange.jkorf.dev/
|
||||
- Shared API V2 migration: https://github.com/JKorf/CryptoExchange.Net/blob/master/docs/SHARED_API_V2_MIGRATION.md
|
||||
- AI API map: https://github.com/JKorf/CryptoExchange.Net/blob/master/docs/ai-api-map.md
|
||||
- CryptoClients.Net: https://github.com/JKorf/CryptoClients.Net
|
||||
- CryptoManager.Net: https://github.com/JKorf/CryptoManager.Net
|
||||
- NuGet: https://www.nuget.org/packages/CryptoExchange.Net
|
||||
- Discord: https://discord.gg/MSpeEtSY8t
|
||||
@@ -1,40 +1,37 @@
|
||||
# CryptoExchange.Net
|
||||
|
||||
> Base C#/.NET library for cryptocurrency exchange API client implementations. Provides a standardized abstraction (REST, WebSocket, authentication, rate limiting, error handling, order book management, shared cross-exchange interfaces) that 28+ exchange-specific libraries are built on top of.
|
||||
> Base C#/.NET library used by 28+ cryptocurrency exchange clients. It standardizes REST, WebSocket, authentication, rate limiting, results, and exchange-agnostic Shared APIs.
|
||||
|
||||
CryptoExchange.Net itself is not used directly — install one of the exchange-specific libraries (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.) or `CryptoClients.Net` to access all exchanges via a single bundle. The base library is what makes the entire ecosystem feel consistent: same `HttpResult<T>` REST result pattern, same `WebSocketResult<UpdateSubscription>` websocket subscription pattern, same DI registration, same shared interfaces across all exchanges. Current version: 12.5.1. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0. Native AOT supported.
|
||||
CryptoExchange.Net is not an exchange client by itself. Install exchange-specific packages such as Binance.Net, Bybit.Net, JK.OKX.Net, Kraken.Net, or Coinbase.Net, or install `CryptoClients.Net` for the bundle. Current release: 13.2.2. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, and net10.0; Native AOT is supported.
|
||||
|
||||
The standout feature for cross-exchange code is `CryptoExchange.Net.SharedApis` — a set of interfaces (`ISpotTickerRestClient`, `ISpotOrderRestClient`, `IBalanceRestClient`, etc.) implemented by every exchange library. Same call signature works against any exchange.
|
||||
## Shared API V2
|
||||
|
||||
Version 12.2.0 adds typed asset metadata to shared symbol discovery. `SharedSpotSymbol` and `SharedFuturesSymbol` expose `DisplayName` plus base/quote `SharedAssetType` and `SharedAssetSubType` values. `GetSymbolsRequest` can filter on those four type fields. After symbol discovery, `ISpotSymbolRestClient.SpotSymbolCatalog` and `IFuturesSymbolRestClient.FuturesSymbolCatalog` provide asset and symbol dictionaries; each catalog is available only after the corresponding `Get*SymbolsAsync` call. `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` are best-effort helpers for exchange-library implementations.
|
||||
For new cross-exchange code, use fine-grained capability interfaces from `CryptoExchange.Net.SharedApis` through each API surface's `.SharedApi` property. Examples: `IGetTickerRest`, `IGetOrderBookRest`, `IPlaceSpotOrderRest`, and `ISubscribeTradesSocket`.
|
||||
|
||||
Version 12.4.0 represents market-data quantities with `SharedOrderQuantity`: use `Volumes` on `SharedSpotTicker`, `SharedFuturesTicker`, and `SharedKline`, and `Quantities` on `SharedTrade`. The old scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete. Exchange-library implementations must pass `SharedOrderQuantity` to these model constructors.
|
||||
Use `SharedSymbol` to normalize symbols and `Task.WhenAll` for independent calls across exchanges. Use an exchange-wide `I[Exchange]SharedApiClient` plus `SharedCapabilities` and `GetCapability` when selecting operations dynamically. A resolution can be `null`; its `Options` describes supported trading modes and request/exchange parameter rules.
|
||||
|
||||
Version 12.5.0 adds optional shared WebSocket order commands through `ISpotOrderManagementSocketClient` and `IFuturesOrderManagementSocketClient`. Their place/cancel methods return `QueryResult<SharedId>`; they are commands rather than update subscriptions, and support is exchange-specific.
|
||||
Transport-agnostic capabilities return `IExchangeCallResult<T>`. REST capabilities return `HttpResult<T>`, socket commands return `QueryResult<T>`, and socket subscriptions return `WebSocketResult<UpdateSubscription>`. Always check `.Success` before `.Data`.
|
||||
|
||||
V1 broad interfaces remain accessible through `.SharedClient` for incremental migration. Prefer `.SharedApi` for new code. V2 ticker calls use `GetTickerAsync`/`GetAllTickersAsync` and the common `SharedTicker` model.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [README](https://github.com/JKorf/CryptoExchange.Net/blob/master/README.md): Overview, full ecosystem table (28+ exchange libraries), installation per exchange, complete release notes
|
||||
- [Documentation Site](https://cryptoexchange.jkorf.dev/): Full documentation hub with sections per topic
|
||||
- [SharedApis Documentation](https://cryptoexchange.jkorf.dev/CryptoExchange.Net/idocs_shared.html): Cross-exchange shared interface guide
|
||||
- [README](https://github.com/JKorf/CryptoExchange.Net/blob/master/README.md): ecosystem overview, packages, and release notes
|
||||
- [Shared API V2 migration](https://github.com/JKorf/CryptoExchange.Net/blob/master/docs/SHARED_API_V2_MIGRATION.md): V1-to-V2 mapping, capability selection, transports, and parameter discovery
|
||||
- [Documentation site](https://cryptoexchange.jkorf.dev/): complete library documentation
|
||||
- [Shared API documentation](https://cryptoexchange.jkorf.dev/CryptoExchange.Net/idocs_shared.html): cross-exchange API guide
|
||||
- [AI API map](https://github.com/JKorf/CryptoExchange.Net/blob/master/docs/ai-api-map.md): concise V2 interface and selection map
|
||||
|
||||
## Examples
|
||||
## AI context files
|
||||
|
||||
- [AI-friendly examples directory](https://github.com/JKorf/CryptoExchange.Net/tree/master/Examples/ai-friendly): Compact, fully runnable examples optimized for AI assistants
|
||||
- [Shared Clients Quickstart](https://github.com/JKorf/CryptoExchange.Net/blob/master/Examples/ai-friendly/01-shared-clients-quickstart.cs): Same code calling multiple exchanges via SharedApis
|
||||
- [Multi-Exchange Tickers](https://github.com/JKorf/CryptoExchange.Net/blob/master/Examples/ai-friendly/02-multi-exchange-tickers.cs): Aggregating ticker data across N exchanges concurrently
|
||||
- [Cross-Exchange Arbitrage Skeleton](https://github.com/JKorf/CryptoExchange.Net/blob/master/Examples/ai-friendly/03-cross-exchange-arbitrage-skeleton.cs): Pattern for building a price difference scanner
|
||||
- [Full Examples Repository](https://github.com/JKorf/CryptoExchange.Net/tree/master/Examples): ConsoleClient with multiple exchange exchanges, BlazorClient, SharedClients
|
||||
- [AGENTS.md](https://github.com/JKorf/CryptoExchange.Net/blob/master/AGENTS.md): practical generation rules and examples
|
||||
- [llms-full.txt](https://github.com/JKorf/CryptoExchange.Net/blob/master/llms-full.txt): expanded library and Shared API V2 context
|
||||
- [.github/copilot-instructions.md](https://github.com/JKorf/CryptoExchange.Net/blob/master/.github/copilot-instructions.md): compact repository conventions
|
||||
- [.cursor/rules/CryptoExchange-Net.mdc](https://github.com/JKorf/CryptoExchange.Net/blob/master/.cursor/rules/CryptoExchange-Net.mdc): Cursor rules for cross-exchange C# code
|
||||
|
||||
## Reference
|
||||
## Related projects
|
||||
|
||||
- [Ecosystem libraries list](https://github.com/JKorf/CryptoExchange.Net#cryptoexchangenet-ecosystem): Aster, Binance, BingX, Bitfinex, Bitget, BitMart, BitMEX, Bitstamp, BloFin, Bybit, Coinbase, CoinEx, CoinW, CoinGecko, Crypto.com, DeepCoin, Gate.io, HTX, HyperLiquid, Kraken, Kucoin, Mexc, OKX, Pionex, Polymarket, Toobit, Upbit, Weex, WhiteBit, XT
|
||||
- [CryptoClients.Net](https://github.com/JKorf/CryptoClients.Net): Single bundle package for all exchange libraries
|
||||
- [CryptoManager.Net](https://github.com/JKorf/CryptoManager.Net): Full demo application using CryptoClients.Net
|
||||
- [NuGet Package](https://www.nuget.org/packages/CryptoExchange.Net): Latest stable release on NuGet
|
||||
|
||||
## Optional
|
||||
|
||||
- [Discord Community](https://discord.gg/MSpeEtSY8t): Maintainer-supported Discord for ecosystem-wide discussion
|
||||
- [GitHub Issues](https://github.com/JKorf/CryptoExchange.Net/issues): Bug reports and feature requests
|
||||
- [GitHub Sponsors](https://github.com/sponsors/JKorf): Support the maintainer
|
||||
- [CryptoClients.Net](https://github.com/JKorf/CryptoClients.Net): bundle and cross-exchange capability lookup
|
||||
- [CryptoManager.Net](https://github.com/JKorf/CryptoManager.Net): full demo application
|
||||
- [NuGet](https://www.nuget.org/packages/CryptoExchange.Net): package releases
|
||||
- [Discord](https://discord.gg/MSpeEtSY8t): maintainer-supported community
|
||||
|
||||
Reference in New Issue
Block a user