1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-11-04 17:27:45 +00:00

Compare commits

..

29 Commits

Author SHA1 Message Date
Jkorf 600131d14d Updated CryptoExchange.Net to version 13.2.3 2026-11-03 14:24:53 +01:00
Jkorf a092277036 Updated CryptoExchange.Net.Web3 to version 13.0.0 2026-11-03 13:47:29 +01:00
Jkorf ae11dd382f Added ReadMe 2026-11-03 13:44:37 +01:00
Jkorf cbe21a86c7 Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2026-11-03 13:40:05 +01:00
nils2525 e45fa235e0 Fixed socket cleanup after connection failures (#301) 2026-11-03 13:39:50 +01:00
nils2525 f88ed02735 Fixed cancellation source disposal race in CloseAsync (#300) 2026-11-03 13:38:38 +01:00
Jkorf 2ef4dd8555 Updated test error messages 2026-11-03 13:34:22 +01:00
Jkorf 7338513f81 Added Web3 library 2026-11-03 13:34:05 +01:00
Jkorf 11ac7e4f0b Added additional constructor for NoApiCredentialsError 2026-10-29 15:38:24 +01:00
Jkorf a4ae3c6d87 Updated examples 2026-10-22 13:55:55 +02:00
Jkorf 3015da911e Updated library list 2026-10-22 13:36:35 +02:00
Jkorf 6df3dcc422 Updated to version 13.2.2 2026-10-22 10:20:14 +02:00
Jkorf 9eceaf1eea Fixed UserDataTracker config not getting set correctly, added warning for missing tracked symbols if open orders request requires it 2026-10-21 15:36:25 +02:00
Jkorf f1ad4e9920 Updated to version 13.2.1 2026-10-19 08:52:09 +02:00
Jkorf ee53dfe139 Fixed subscription without any predefined routes failing to subscribe 2026-10-19 08:39:39 +02:00
Jkorf 186f30d4b5 Fix test validation failing with empty body string 2026-10-19 08:39:18 +02:00
Jkorf f079e6f0f4 Updated to version 13.2.0 2026-10-13 11:41:36 +02:00
Jkorf c6647f6815 Fixed DelayAfterConnect applying on failed connection attemps on HighPerf connections 2026-10-13 10:38:21 +02:00
Jkorf 5135047097 Fix for duplicate subscription coverage 2026-10-13 10:37:49 +02:00
Jkorf 4f25a6e7d6 Added processor parameter for some methods in SocketApiClient to allow specific overriding behavior 2026-10-13 10:08:16 +02:00
Jan Korf a0f3dcaa4c WebSocket covered subscriptions
Updated WebSocket duplicate subscription handling
Fixed incorrect status keeping of WebSocket subscriptions which don't expect a response
2026-10-12 13:19:44 +02:00
Jkorf 550afd41c3 Added DelayAfterConnect to SocketApiOptions, fixed DelayAfterConnect not being respected on reconnection 2026-10-02 15:42:35 +02:00
Jkorf afdced3bb8 Added Shared API ISubscribeFundingInfoSocket 2026-10-02 11:09:07 +02:00
Jkorf f8c657f308 Updated Shared APIs ISubscribeIncrementalOrderBook 2026-10-02 09:33:15 +02:00
Jkorf c9ae1bf379 Updated to version 13.1.0 2026-09-28 11:23:36 +02:00
Jkorf 59acbd648e Added UnsubscribeAllAsync method to Shared APIs ISharedSubscription and IShaerdApiClientBase 2026-09-28 11:11:41 +02:00
Jkorf 7a11bd0c5e Added WaitAllAsync extension method 2026-09-25 09:33:45 +02:00
Jkorf 9502db6d58 Added SubscribeAllAsync extension methods for Shared API resolved capabilities 2026-09-25 08:32:57 +02:00
Jkorf e6ab899654 Updated examples 2026-09-24 11:41:44 +02:00
50 changed files with 2250 additions and 196 deletions
+1
View File
@@ -287,3 +287,4 @@ __pycache__/
*.odx.cs *.odx.cs
*.xsd.cs *.xsd.cs
CryptoExchange.Net/CryptoExchange.Net.xml CryptoExchange.Net/CryptoExchange.Net.xml
CryptoExchange.Net.Web3/CryptoExchange.Net.Web3.xml
+149
View File
@@ -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; } = [];
}
}
+382
View File
@@ -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>
+171
View File
@@ -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);
}
}
}
}
+8
View File
@@ -0,0 +1,8 @@
# ![.CryptoExchange.Net.Web3](https://github.com/JKorf/CryptoExchange.Net/blob/ffcb7db8ff597c2f14982d68464015a748815580/CryptoExchange.Net/Icon/icon.png) CryptoExchange.Net.Web3
Web3 utilities for CryptoExchange.Net implementations.
## Release notes
* Version 13.0.0 - 03 Nov 2026
* Initial release
+8 -2
View File
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 18
VisualStudioVersion = 17.0.32014.148 VisualStudioVersion = 18.10.12201.205 stable
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoExchange.Net", "CryptoExchange.Net\CryptoExchange.Net.csproj", "{3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoExchange.Net", "CryptoExchange.Net\CryptoExchange.Net.csproj", "{3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}"
EndProject EndProject
@@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\C
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptoExchange.Net.Web3", "CryptoExchange.Net.Web3\CryptoExchange.Net.Web3.csproj", "{7ABCD210-C708-42ED-AF81-85C0A6089A1E}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
+74 -14
View File
@@ -286,7 +286,7 @@ namespace CryptoExchange.Net.Clients
while (true) while (true)
{ {
// Get a new or existing socket connection // 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) if (!socketResult.Success)
return WebSocketResult.Fail<UpdateSubscription>(Exchange, socketResult.Error); return WebSocketResult.Fail<UpdateSubscription>(Exchange, socketResult.Error);
@@ -309,7 +309,7 @@ namespace CryptoExchange.Net.Clients
var needsConnecting = !socketConnection.Connected; 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) if (!connectResult.Success)
return WebSocketResult.Fail<UpdateSubscription>(Exchange, connectResult.Error!); return WebSocketResult.Fail<UpdateSubscription>(Exchange, connectResult.Error!);
@@ -482,7 +482,7 @@ namespace CryptoExchange.Net.Clients
await semaphoreSlim.WaitAsync().ConfigureAwait(false); await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try 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) if (!socketResult.Success)
return QueryResult.Fail<THandlerResponse>(Exchange, socketResult.Error); return QueryResult.Fail<THandlerResponse>(Exchange, socketResult.Error);
@@ -495,7 +495,7 @@ namespace CryptoExchange.Net.Clients
released = true; 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) if (!connectResult.Success)
return QueryResult.Fail<THandlerResponse>(Exchange, connectResult.Error!); 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="authenticated">Whether the socket should authenticated</param>
/// <param name="ct">Cancellation token</param> /// <param name="ct">Cancellation token</param>
/// <returns></returns> /// <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) if (socket.Connected)
return CallResult.Ok(); return CallResult.Ok();
@@ -533,16 +544,13 @@ namespace CryptoExchange.Net.Clients
if (!connectResult.Success) if (!connectResult.Success)
return connectResult; return connectResult;
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
if (!authenticated || socket.Authenticated) if (!authenticated || socket.Authenticated)
return CallResult.Ok(); return CallResult.Ok();
if (socket is not SocketConnection sc) if (socket is not SocketConnection sc)
throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication"); 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) if (!result.Success)
await socket.CloseAsync().ConfigureAwait(false); await socket.CloseAsync().ConfigureAwait(false);
@@ -554,13 +562,22 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
/// <param name="socket">Socket to authenticate</param> /// <param name="socket">Socket to authenticate</param>
/// <returns></returns> /// <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) if (GetAuthenticationProvider() == null)
return CallResult.Fail(new NoApiCredentialsError()); return CallResult.Fail(new NoApiCredentialsError());
_logger.AttemptingToAuthenticate(socket.SocketId); _logger.AttemptingToAuthenticate(socket.SocketId);
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false); var authRequest = await GetAuthenticationRequestAsync(socket, processor).ConfigureAwait(false);
if (authRequest != null) if (authRequest != null)
{ {
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false); var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
@@ -581,6 +598,7 @@ namespace CryptoExchange.Net.Clients
return CallResult.Ok(); return CallResult.Ok();
} }
/// <summary> /// <summary>
/// Should return the request which can be used to authenticate a socket connection /// Should return the request which can be used to authenticate a socket connection
/// </summary> /// </summary>
@@ -588,6 +606,13 @@ namespace CryptoExchange.Net.Clients
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) =>
Task.FromResult(GetAuthenticationProvider()!.GetAuthenticationQuery(this, 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> /// <summary>
/// Adds a system subscription. Used for example to reply to ping requests /// Adds a system subscription. Used for example to reply to ping requests
/// </summary> /// </summary>
@@ -645,6 +670,18 @@ namespace CryptoExchange.Net.Clients
&& (AllowTopicsOnTheSameConnection || !connection.Topics.Contains(topic)); && (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> /// <summary>
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one. /// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
/// </summary> /// </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="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="individualSubscriptionCount">The number of individual subscriptions in this subscribe request</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection( protected virtual Task<CallResult<SocketConnection>> GetSocketConnection(
string address, string address,
bool authenticated, bool authenticated,
bool dedicatedRequestConnection, bool dedicatedRequestConnection,
CancellationToken ct, CancellationToken ct,
string? topic = null, string? topic = null,
int individualSubscriptionCount = 1) 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 // If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
var delayStart = DateTime.UtcNow; var delayStart = DateTime.UtcNow;
@@ -841,6 +899,8 @@ namespace CryptoExchange.Net.Clients
return connectResult; return connectResult;
} }
// Remove the connection and stop any reconnect processing before disposing it.
await socketConnection.CloseAsync().ConfigureAwait(false);
socketConnection.Dispose(); socketConnection.Dispose();
return connectResult; return connectResult;
} }
@@ -862,7 +922,7 @@ namespace CryptoExchange.Net.Clients
RateLimitAdmissionCallbackRequest = () => AdmissionOverride.Value, RateLimitAdmissionCallbackRequest = () => AdmissionOverride.Value,
Proxy = ClientOptions.Proxy, Proxy = ClientOptions.Proxy,
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout, Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
ReceiveBufferSize = ClientOptions.ReceiveBufferSize, ReceiveBufferSize = ClientOptions.ReceiveBufferSize
}; };
/// <summary> /// <summary>
+3 -3
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <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> <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>13.0.0</PackageVersion> <PackageVersion>13.2.3</PackageVersion>
<AssemblyVersion>13.0.0</AssemblyVersion> <AssemblyVersion>13.2.3</AssemblyVersion>
<FileVersion>13.0.0</FileVersion> <FileVersion>13.2.3</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <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> <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> <RepositoryType>git</RepositoryType>
+5
View File
@@ -138,6 +138,11 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
public NoApiCredentialsError() : base(null, _errorInfo, null) { } public NoApiCredentialsError() : base(null, _errorInfo, null) { }
/// <summary>
/// ctor
/// </summary>
public NoApiCredentialsError(string message) : base(null, _errorInfo with { Message = message }, null) { }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@@ -19,6 +19,11 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary> /// </summary>
public int? MaxSocketConnections { get; set; } 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> /// <summary>
/// Set the values of this options on the target options /// Set the values of this options on the target options
/// </summary> /// </summary>
@@ -28,6 +33,7 @@ namespace CryptoExchange.Net.Objects.Options
item.SocketNoDataTimeout = SocketNoDataTimeout; item.SocketNoDataTimeout = SocketNoDataTimeout;
item.AutoTimestamp = AutoTimestamp; item.AutoTimestamp = AutoTimestamp;
item.MaxSocketConnections = MaxSocketConnections; item.MaxSocketConnections = MaxSocketConnections;
item.DelayAfterConnect = DelayAfterConnect;
return item; return item;
} }
} }
@@ -23,6 +23,10 @@ namespace CryptoExchange.Net.SharedApis
/// Get user funding history capability. /// Get user funding history capability.
/// </summary> /// </summary>
public static SharedRestCapability<IGetUserFundingHistory, IGetUserFundingHistoryRest> GetUserFundingHistory { get; } = new(); 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.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
@@ -104,6 +105,11 @@ namespace CryptoExchange.Net.SharedApis
SharedCapabilityResolution<T>? GetCapability<T>(SharedCapabilityReference<T> capability, TradingMode tradingMode) SharedCapabilityResolution<T>? GetCapability<T>(SharedCapabilityReference<T> capability, TradingMode tradingMode)
where T : ISharedApiCapability; where T : ISharedApiCapability;
/// <summary>
/// Unsubscribes all subscriptions created by the underlying socket API
/// client and closes its subscription connections.
/// </summary>
Task UnsubscribeAllAsync();
} }
/// <summary> /// <summary>
@@ -209,6 +215,15 @@ namespace CryptoExchange.Net.SharedApis
return GetCapabilities<T>(tradingMode); return GetCapabilities<T>(tradingMode);
} }
/// <inheritdoc />
public Task UnsubscribeAllAsync()
{
return Task.WhenAll(
_sharedApis
.OfType<ISharedSubscription>()
.Select(x => x.UnsubscribeAllAsync()));
}
object? ISharedApiClientResolver.GetCapability(Type capabilityType) object? ISharedApiClientResolver.GetCapability(Type capabilityType)
{ {
return _sharedApis return _sharedApis
@@ -1,10 +1,12 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
@@ -624,5 +626,212 @@ namespace CryptoExchange.Net.SharedApis
.ParallelEnumerateAsync(); .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();
}
} }
} }
@@ -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);
}
}
@@ -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)
{
}
}
}
@@ -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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Shared WebSocket API subscription capabitlity /// Shared WebSocket API subscription capability
/// </summary> /// </summary>
public interface ISharedSubscription : ISharedSocket public interface ISharedSubscription : ISharedSocket
{ {
/// <summary>
/// Unsubscribes all subscriptions created by the underlying socket API
/// client and closes its subscription connections.
/// </summary>
Task UnsubscribeAllAsync();
} }
} }
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Order book subscription options /// Order book subscription options
/// </summary> /// </summary>
SubscribeOrderBookOptions SubscribeOrderBookOptions { get; } SubscribeIncrementalOrderBookOptions SubscribeIncrementalOrderBookOptions { get; }
/// <summary> /// <summary>
/// Subscribe to incremental order book updates for a symbol /// 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="handler">Update handler</param>
/// <param name="ct">Cancellation token, can be used to stop the updates</param> /// <param name="ct">Cancellation token, can be used to stop the updates</param>
/// <returns></returns> /// <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);
} }
} }
@@ -33,7 +33,7 @@ namespace CryptoExchange.Net.SharedApis
/// ctor /// ctor
/// </summary> /// </summary>
public SubscribeIncrementalOrderBookOptions(string exchange, bool needsAuthentication, int[] limits, SharedOrderBookSubscriptionType updateType) 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; SupportedLimits = limits;
UpdateType = updateType; UpdateType = updateType;
@@ -446,27 +446,28 @@ namespace CryptoExchange.Net.Sockets.Default
if (_closeTask?.IsCompleted == false) if (_closeTask?.IsCompleted == false)
{ {
_logger.SocketCloseAsyncWaitingForExistingCloseTask(Id); _logger.SocketCloseAsyncWaitingForExistingCloseTask(Id);
await _closeTask.ConfigureAwait(false);
return;
} }
else
if (!IsOpen)
{ {
if (!IsOpen)
_logger.SocketCloseAsyncSocketNotOpen(Id); _logger.SocketCloseAsyncSocketNotOpen(Id);
return; else
}
_logger.SocketClosing(Id); _logger.SocketClosing(Id);
_closeTask = CloseInternalAsync(); _closeTask = CloseInternalAsync();
} }
}
finally finally
{ {
_closeSem.Release(); _closeSem.Release();
} }
await _closeTask.ConfigureAwait(false); await _closeTask.ConfigureAwait(false);
// Processing may still be running after the socket closes; finish before the caller disposes it.
if(_processTask != null) if(_processTask != null)
await _processTask.ConfigureAwait(false); await _processTask.ConfigureAwait(false);
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
_logger.SocketClosed(Id); _logger.SocketClosed(Id);
} }
@@ -424,6 +424,9 @@ namespace CryptoExchange.Net.Sockets.Default
{ {
try 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); var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false);
if (!reconnectSuccessful.Success) if (!reconnectSuccessful.Success)
{ {
@@ -616,7 +619,17 @@ namespace CryptoExchange.Net.Sockets.Default
/// Connect the websocket /// Connect the websocket
/// </summary> /// </summary>
/// <returns></returns> /// <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> /// <summary>
/// Retrieve the underlying socket /// Retrieve the underlying socket
@@ -689,7 +702,7 @@ namespace CryptoExchange.Net.Sockets.Default
if (subscription.CancellationTokenRegistration.HasValue) if (subscription.CancellationTokenRegistration.HasValue)
subscription.CancellationTokenRegistration.Value.Dispose(); 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; bool shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Status == SubscriptionStatus.Closing || r.Status == SubscriptionStatus.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
if (!anyDuplicateSubscription) if (!anyDuplicateSubscription)
@@ -1056,6 +1069,36 @@ namespace CryptoExchange.Net.Sockets.Default
return CallResult.Ok(); 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> /// <summary>
/// Try to subscribe a new subscription by sending the subscribe query and wait for the result as needed /// Try to subscribe a new subscription by sending the subscribe query and wait for the result as needed
/// </summary> /// </summary>
@@ -1090,11 +1133,11 @@ namespace CryptoExchange.Net.Sockets.Default
return WebSocketResult.Ok(ApiClient.Exchange, SocketId, default, 0, ConnectionUriString); 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); subscription.HandleSubQueryResponse(this, subQuery.Response);
if (newSubscription && subQuery.Success) if (newSubscription && success)
{ {
subscription.TokenExpired += HandleTokenExpired; subscription.TokenExpired += HandleTokenExpired;
if (subCancelToken != default) if (subCancelToken != default)
@@ -1109,6 +1152,14 @@ namespace CryptoExchange.Net.Sockets.Default
}; };
subQuery.OnComplete = subCompleteHandler; 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); var subQueryResult = await SendAndWaitQueryAsync(subQuery).ConfigureAwait(false);
if (!subQueryResult.Success) if (!subQueryResult.Success)
{ {
@@ -1119,9 +1170,6 @@ namespace CryptoExchange.Net.Sockets.Default
return WebSocketResult.Fail(ApiClient.Exchange, subQueryResult.Error!); return WebSocketResult.Fail(ApiClient.Exchange, subQueryResult.Error!);
} }
if (!subQuery.ExpectsResponse)
subCompleteHandler();
return subQueryResult; return subQueryResult;
} }
@@ -198,8 +198,13 @@ namespace CryptoExchange.Net.Sockets.HighPerf
{ {
var result = await _socket.ConnectAsync(ct).ConfigureAwait(false); var result = await _socket.ConnectAsync(ct).ConfigureAwait(false);
if (result.Success) if (result.Success)
{
_processTask = ProcessAsync(_cts.Token); _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; return result;
} }
+22 -6
View File
@@ -124,7 +124,7 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// On complete callback /// On complete callback
/// </summary> /// </summary>
public Action? OnComplete { get; set; } public Action<bool>? OnComplete { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public event Action? OnMessageRouterUpdated; public event Action? OnMessageRouterUpdated;
@@ -157,8 +157,7 @@ namespace CryptoExchange.Net.Sockets
} }
else else
{ {
Completed = true; Complete();
_event.Set();
} }
} }
@@ -175,6 +174,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public abstract void Timeout(); public abstract void Timeout();
/// <summary>
/// Mark request as successfully completed
/// </summary>
public abstract void Complete();
/// <summary> /// <summary>
/// Mark request as failed /// Mark request as failed
/// </summary> /// </summary>
@@ -248,7 +252,7 @@ namespace CryptoExchange.Net.Sockets
{ {
Completed = true; Completed = true;
_event.Set(); _event.Set();
OnComplete?.Invoke(); OnComplete?.Invoke(Success);
} }
return handled; return handled;
@@ -267,7 +271,19 @@ namespace CryptoExchange.Net.Sockets
Completed = true; Completed = true;
_event.Set(); _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 /> /// <inheritdoc />
@@ -280,7 +296,7 @@ namespace CryptoExchange.Net.Sockets
Completed = true; Completed = true;
_event.Set(); _event.Set();
OnComplete?.Invoke(); OnComplete?.Invoke(Success);
} }
} }
} }
@@ -168,12 +168,18 @@ namespace CryptoExchange.Net.Testing
{ {
// Validate request body // Validate request body
Dictionary<string, object> bodyParameters; 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!)!; bodyParameters = JsonSerializer.Deserialize<Dictionary<string, object>>(result.RequestBody!)!;
} }
else else
{ {
// Form data
var splitKvp = result.RequestBody.Split('&'); var splitKvp = result.RequestBody.Split('&');
bodyParameters = splitKvp.Select(x => x.Split('=')).ToDictionary(x => x[0], x => (object)x[1]); 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) private void CompareParameters(Dictionary<string, object> expectedUrlParameters, Dictionary<string, object> parameters, List<string>? ignoreParamValidation)
{ {
if (expectedUrlParameters.Count > parameters.Count) 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) foreach (var kvp in expectedUrlParameters)
{ {
@@ -204,10 +210,10 @@ namespace CryptoExchange.Net.Testing
continue; continue;
if (!parameters.TryGetValue(kvp.Key, out var value)) 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)) 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, SharedAccountType accountType,
TrackerItemConfig config, TrackerItemConfig config,
ExchangeParameters? exchangeParameters = null 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 }; config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
SetConfig(config);
_restClient = restClient; _restClient = restClient;
_socketClient = socketClient; _socketClient = socketClient;
@@ -24,6 +24,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
private readonly Dictionary<string, int> _openOrderNotReturnedTimes = new(); private readonly Dictionary<string, int> _openOrderNotReturnedTimes = new();
private readonly TimeSpan _pollOverlapPeriod = TimeSpan.FromSeconds(3); private readonly TimeSpan _pollOverlapPeriod = TimeSpan.FromSeconds(3);
private bool _openOrderWarningDone = false;
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate; internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
/// <summary> /// <summary>
@@ -39,10 +40,11 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
IEnumerable<SharedSymbol> symbols, IEnumerable<SharedSymbol> symbols,
bool onlyTrackProvidedSymbols, bool onlyTrackProvidedSymbols,
ExchangeParameters? exchangeParameters = null 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 }; config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
SetConfig(config);
_openOrdersClient = openRestClient; _openOrdersClient = openRestClient;
_closedOrdersClient = closedRestClient; _closedOrdersClient = closedRestClient;
@@ -234,7 +236,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
} }
else 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); var openOrdersResult = await _openOrdersClient.GetOpenFuturesOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
if (!openOrdersResult.Success) if (!openOrdersResult.Success)
@@ -34,10 +34,11 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
IEnumerable<SharedSymbol> symbols, IEnumerable<SharedSymbol> symbols,
bool onlyTrackProvidedSymbols, bool onlyTrackProvidedSymbols,
ExchangeParameters? exchangeParameters = null 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 }; config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
SetConfig(config);
_restClient = restClient; _restClient = restClient;
_socketClient = socketClient; _socketClient = socketClient;
@@ -37,10 +37,11 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
bool onlyTrackProvidedSymbols, bool onlyTrackProvidedSymbols,
bool websocketPositionUpdatesAreFullSnapshots, bool websocketPositionUpdatesAreFullSnapshots,
ExchangeParameters? exchangeParameters = null 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 }; config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
SetConfig(config);
_restClient = restClient; _restClient = restClient;
_socketClient = socketClient; _socketClient = socketClient;
@@ -23,6 +23,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
private readonly bool _timeFilterSupportedClosedOrders; private readonly bool _timeFilterSupportedClosedOrders;
private readonly Dictionary<string, int> _openOrderNotReturnedTimes = new(); private readonly Dictionary<string, int> _openOrderNotReturnedTimes = new();
private readonly TimeSpan _pollOverlapPeriod = TimeSpan.FromSeconds(3); private readonly TimeSpan _pollOverlapPeriod = TimeSpan.FromSeconds(3);
private bool _openOrderWarningDone = false;
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate; internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
@@ -39,10 +40,11 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
IEnumerable<SharedSymbol> symbols, IEnumerable<SharedSymbol> symbols,
bool onlyTrackProvidedSymbols, bool onlyTrackProvidedSymbols,
ExchangeParameters? exchangeParameters = null 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 }; config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
SetConfig(config);
_openOrderClient = openOrderClient; _openOrderClient = openOrderClient;
_closedOrderClient = closedOrderClient; _closedOrderClient = closedOrderClient;
@@ -244,7 +246,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
} }
else 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); var openOrdersResult = await _openOrderClient.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
if (!openOrdersResult.Success) if (!openOrdersResult.Success)
@@ -34,10 +34,11 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
IEnumerable<SharedSymbol> symbols, IEnumerable<SharedSymbol> symbols,
bool onlyTrackProvidedSymbols, bool onlyTrackProvidedSymbols,
ExchangeParameters? exchangeParameters = null 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 }; config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
SetConfig(config);
_restClient = restClient; _restClient = restClient;
_socketClient = socketClient; _socketClient = socketClient;
@@ -242,15 +242,21 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
ILogger logger, ILogger logger,
UserDataSymbolTracker symbolTracker, UserDataSymbolTracker symbolTracker,
UserDataType dataType, UserDataType dataType,
string exchange, string exchange) : base(logger, dataType, exchange)
TrackerItemConfig config) : 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; _pollIntervalDisconnected = config.PollIntervalDisconnected;
_pollIntervalConnected = config.PollIntervalConnected; _pollIntervalConnected = config.PollIntervalConnected;
_pollAtStart = config.PollAtStart; _pollAtStart = config.PollAtStart;
_retentionTime = config is TrackerTimedItemConfig timeConfig ? timeConfig.RetentionTime : TimeSpan.MaxValue; _retentionTime = config is TrackerTimedItemConfig timeConfig ? timeConfig.RetentionTime : TimeSpan.MaxValue;
_isSymbolModel = typeof(T).IsSubclassOf(typeof(SharedSymbolModel));
_symbolTracker = symbolTracker;
} }
/// <summary> /// <summary>
+29 -30
View File
@@ -5,37 +5,36 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="13.5.0" /> <PackageReference Include="Binance.Net" Version="13.7.0" />
<PackageReference Include="Bitfinex.Net" Version="11.4.0" /> <PackageReference Include="Bitfinex.Net" Version="11.7.1" />
<PackageReference Include="BitMart.Net" Version="4.4.0" /> <PackageReference Include="Bitstamp.Net" Version="2.6.0" />
<PackageReference Include="BloFin.Net" Version="3.4.0" /> <PackageReference Include="Bitunix.Net" Version="1.0.1" />
<PackageReference Include="Bybit.Net" Version="7.5.0" /> <PackageReference Include="BloFin.Net" Version="3.6.0" />
<PackageReference Include="CoinEx.Net" Version="11.4.0" /> <PackageReference Include="Bybit.Net" Version="7.7.0" />
<PackageReference Include="CoinW.Net" Version="3.4.0" /> <PackageReference Include="CoinW.Net" Version="3.6.2" />
<PackageReference Include="CryptoCom.Net" Version="4.4.0" /> <PackageReference Include="CryptoCom.Net" Version="4.6.0" />
<PackageReference Include="DeepCoin.Net" Version="4.4.0" /> <PackageReference Include="DeepCoin.Net" Version="4.6.1" />
<PackageReference Include="GateIo.Net" Version="4.6.0" /> <PackageReference Include="GateIo.Net" Version="4.8.0" />
<PackageReference Include="HyperLiquid.Net" Version="5.7.0" /> <PackageReference Include="HyperLiquid.Net" Version="5.9.1" />
<PackageReference Include="JK.BingX.Net" Version="4.5.0" /> <PackageReference Include="JK.BingX.Net" Version="4.7.0" />
<PackageReference Include="JK.Bitget.Net" Version="4.5.0" /> <PackageReference Include="JK.Bitget.Net" Version="4.9.0" />
<PackageReference Include="JK.Mexc.Net" Version="6.6.0" /> <PackageReference Include="JK.Mexc.Net" Version="6.8.0" />
<PackageReference Include="JK.OKX.Net" Version="5.4.0" /> <PackageReference Include="JK.OKX.Net" Version="5.7.0" />
<PackageReference Include="Jkorf.Aster.Net" Version="4.4.0" /> <PackageReference Include="Jkorf.Aster.Net" Version="4.6.0" />
<PackageReference Include="JKorf.BitMEX.Net" Version="4.4.0" /> <PackageReference Include="JKorf.Coinbase.Net" Version="4.7.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="4.5.0" /> <PackageReference Include="JKorf.HTX.Net" Version="9.7.0" />
<PackageReference Include="JKorf.HTX.Net" Version="9.5.0" /> <PackageReference Include="JKorf.Lighter.Net" Version="1.7.0" />
<PackageReference Include="JKorf.Lighter.Net" Version="1.5.0" /> <PackageReference Include="JKorf.Upbit.Net" Version="3.6.0" />
<PackageReference Include="JKorf.Upbit.Net" Version="3.4.0" /> <PackageReference Include="KrakenExchange.Net" Version="8.6.0" />
<PackageReference Include="KrakenExchange.Net" Version="8.4.0" /> <PackageReference Include="Kucoin.Net" Version="9.8.0" />
<PackageReference Include="Kucoin.Net" Version="9.5.0" /> <PackageReference Include="LBank.Net" Version="1.3.0" />
<PackageReference Include="LBank.Net" Version="1.1.0" /> <PackageReference Include="Pionex.Net" Version="1.4.0" />
<PackageReference Include="Pionex.Net" Version="1.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Tapbit.Net" Version="1.0.0" /> <PackageReference Include="Tapbit.Net" Version="1.2.0" />
<PackageReference Include="Toobit.Net" Version="4.4.0" /> <PackageReference Include="Toobit.Net" Version="4.6.0" />
<PackageReference Include="Weex.Net" Version="2.4.0" /> <PackageReference Include="Weex.Net" Version="2.6.0" />
<PackageReference Include="WhiteBit.Net" Version="4.4.0" /> <PackageReference Include="WhiteBit.Net" Version="4.7.0" />
<PackageReference Include="XT.Net" Version="4.5.0" /> <PackageReference Include="XT.Net" Version="4.7.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+11 -14
View File
@@ -1,15 +1,16 @@
@page "/" @page "/"
@using Bitstamp.Net.Interfaces.Clients
@using Bitunix.Net.Interfaces.Clients
@inject IAsterRestClient asterClient @inject IAsterRestClient asterClient
@inject IBinanceRestClient binanceClient @inject IBinanceRestClient binanceClient
@inject IBingXRestClient bingXClient @inject IBingXRestClient bingXClient
@inject IBitfinexRestClient bitfinexClient @inject IBitfinexRestClient bitfinexClient
@inject IBitgetRestClient bitgetClient @inject IBitgetRestClient bitgetClient
@inject IBitMartRestClient bitmartClient @inject IBitstampRestClient bitstampClient
@inject IBitMEXRestClient bitmexClient @inject IBitunixRestClient bitunixClient
@inject IBloFinRestClient bloFinClient @inject IBloFinRestClient bloFinClient
@inject IBybitRestClient bybitClient @inject IBybitRestClient bybitClient
@inject ICoinbaseRestClient coinbaseClient @inject ICoinbaseRestClient coinbaseClient
@inject ICoinExRestClient coinexClient
@inject ICoinWRestClient coinWClient @inject ICoinWRestClient coinWClient
@inject ICryptoComRestClient cryptocomClient @inject ICryptoComRestClient cryptocomClient
@inject IDeepCoinRestClient deepCoinClient @inject IDeepCoinRestClient deepCoinClient
@@ -46,12 +47,11 @@
var bingXTask = bingXClient.SpotApi.ExchangeData.GetTickersAsync("BTC-USDT"); var bingXTask = bingXClient.SpotApi.ExchangeData.GetTickersAsync("BTC-USDT");
var bitfinexTask = bitfinexClient.ExchangeApi.ExchangeData.GetTickerAsync("tBTCUSD"); var bitfinexTask = bitfinexClient.ExchangeApi.ExchangeData.GetTickerAsync("tBTCUSD");
var bitgetTask = bitgetClient.SpotApiV2.ExchangeData.GetTickersAsync("BTCUSDT"); var bitgetTask = bitgetClient.SpotApiV2.ExchangeData.GetTickersAsync("BTCUSDT");
var bitmartTask = bitmartClient.SpotApi.ExchangeData.GetTickerAsync("BTC_USDT"); var bitstampTask = bitstampClient.ExchangeApi.ExchangeData.GetTickerAsync("BTCUSDT");
var bitmexTask = bitmexClient.ExchangeApi.ExchangeData.GetSymbolsAsync("XBT_USDT"); var bitunixTask = bitunixClient.SpotApi.ExchangeData.GetLastPriceAsync("BTCUSDT");
var bloFinTask = bloFinClient.FuturesApi.ExchangeData.GetTickersAsync("BTC-USDT"); var bloFinTask = bloFinClient.FuturesApi.ExchangeData.GetTickersAsync("BTC-USDT");
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT"); var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
var coinbaseTask = coinbaseClient.AdvancedTradeApi.ExchangeData.GetSymbolAsync("BTC-USDT"); var coinbaseTask = coinbaseClient.AdvancedTradeApi.ExchangeData.GetSymbolAsync("BTC-USDT");
var coinexTask = coinexClient.SpotApiV2.ExchangeData.GetTickersAsync(["BTCUSDT"]);
var coinWTask = coinWClient.SpotApi.ExchangeData.GetTickersAsync(); var coinWTask = coinWClient.SpotApi.ExchangeData.GetTickersAsync();
var cryptocomTask = cryptocomClient.ExchangeApi.ExchangeData.GetTickersAsync("BTC_USDT"); var cryptocomTask = cryptocomClient.ExchangeApi.ExchangeData.GetTickersAsync("BTC_USDT");
var deepCoinTask = deepCoinClient.ExchangeApi.ExchangeData.GetTickersAsync(DeepCoin.Net.Enums.SymbolType.Spot); var deepCoinTask = deepCoinClient.ExchangeApi.ExchangeData.GetTickersAsync(DeepCoin.Net.Enums.SymbolType.Spot);
@@ -71,7 +71,7 @@
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync(); var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt"); 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); htxTask, krakenTask, kucoinTask, lBankTask, mexcTask, okxTask, pionexTask, toobitTask, upbitTask, weexTask, whitebitTask, xtTask);
if (asterTask.Result.Success) if (asterTask.Result.Success)
@@ -89,11 +89,11 @@
if (bitgetTask.Result.Success) if (bitgetTask.Result.Success)
_prices.Add("Bitget", bitgetTask.Result.Data.Single().LastPrice); _prices.Add("Bitget", bitgetTask.Result.Data.Single().LastPrice);
if (bitmartTask.Result.Success) if (bitstampTask.Result.Success)
_prices.Add("BitMart", bitmartTask.Result.Data.LastPrice); _prices.Add("Bitstamp", bitstampTask.Result.Data.LastPrice);
if (bitmexTask.Result.Success) if (bitunixTask.Result.Success)
_prices.Add("BitMEX", bitmexTask.Result.Data.First().LastPrice); _prices.Add("Bitunix", bitunixTask.Result.Data);
if (bloFinTask.Result.Success) if (bloFinTask.Result.Success)
_prices.Add("BloFin", bloFinTask.Result.Data.First().LastPrice); _prices.Add("BloFin", bloFinTask.Result.Data.First().LastPrice);
@@ -104,9 +104,6 @@
if (coinbaseTask.Result.Success) if (coinbaseTask.Result.Success)
_prices.Add("Coinbase", coinbaseTask.Result.Data.LastPrice ?? 0); _prices.Add("Coinbase", coinbaseTask.Result.Data.LastPrice ?? 0);
if (coinexTask.Result.Success)
_prices.Add("CoinEx", coinexTask.Result.Data.Single().LastPrice);
if (coinWTask.Result.Success) if (coinWTask.Result.Success)
_prices.Add("CoinW", coinWTask.Result.Data.Single(x => x.Symbol == "BTC_USDT").LastPrice); _prices.Add("CoinW", coinWTask.Result.Data.Single(x => x.Symbol == "BTC_USDT").LastPrice);
+4 -6
View File
@@ -4,12 +4,11 @@
@inject IBingXSocketClient bingXSocketClient @inject IBingXSocketClient bingXSocketClient
@inject IBitfinexSocketClient bitfinexSocketClient @inject IBitfinexSocketClient bitfinexSocketClient
@inject IBitgetSocketClient bitgetSocketClient @inject IBitgetSocketClient bitgetSocketClient
@inject IBitMartSocketClient bitmartSocketClient @inject IBitstampSocketClient bitstampSocketClient
@inject IBitMEXSocketClient bitmexSocketClient @inject IBitunixSocketClient bitunixSocketClient
@inject IBloFinSocketClient bloFinSocketClient @inject IBloFinSocketClient bloFinSocketClient
@inject IBybitSocketClient bybitSocketClient @inject IBybitSocketClient bybitSocketClient
@inject ICoinbaseSocketClient coinbaseSocketClient @inject ICoinbaseSocketClient coinbaseSocketClient
@inject ICoinExSocketClient coinExSocketClient
@inject ICoinWSocketClient coinWSocketClient @inject ICoinWSocketClient coinWSocketClient
@inject ICryptoComSocketClient cryptocomSocketClient @inject ICryptoComSocketClient cryptocomSocketClient
@inject IDeepCoinSocketClient deepCoinSocketClient @inject IDeepCoinSocketClient deepCoinSocketClient
@@ -29,6 +28,8 @@
@inject IWhiteBitSocketClient whitebitSocketClient @inject IWhiteBitSocketClient whitebitSocketClient
@inject IXTSocketClient xtSocketClient @inject IXTSocketClient xtSocketClient
@using System.Collections.Concurrent @using System.Collections.Concurrent
@using Bitstamp.Net.Interfaces.Clients
@using Bitunix.Net.Interfaces.Clients
@using CryptoExchange.Net.Objects @using CryptoExchange.Net.Objects
@using CryptoExchange.Net.Objects.Sockets; @using CryptoExchange.Net.Objects.Sockets;
@using CryptoExchange.Net.Sockets @using CryptoExchange.Net.Sockets
@@ -55,12 +56,9 @@
bingXSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BingX", data.Data.LastPrice)), bingXSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BingX", data.Data.LastPrice)),
bitfinexSocketClient.ExchangeApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)), bitfinexSocketClient.ExchangeApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
bitgetSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.First().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 // BloFin doesn't support the ETH/BTC pair
//bloFinSocketClient.FuturesApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BloFin", data.Data.LastPrice)), //bloFinSocketClient.FuturesApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BloFin", data.Data.LastPrice)),
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", 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)), coinWSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CoinW", data.Data.LastPrice)),
coinbaseSocketClient.AdvancedTradeApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Coinbase", data.Data.LastPrice ?? 0)), coinbaseSocketClient.AdvancedTradeApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Coinbase", data.Data.LastPrice ?? 0)),
cryptocomSocketClient.ExchangeApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CryptoCom", data.Data.LastPrice ?? 0)), cryptocomSocketClient.ExchangeApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CryptoCom", data.Data.LastPrice ?? 0)),
+6 -9
View File
@@ -6,11 +6,10 @@
@using BingX.Net.Interfaces @using BingX.Net.Interfaces
@using Bitfinex.Net.Interfaces @using Bitfinex.Net.Interfaces
@using Bitget.Net.Interfaces; @using Bitget.Net.Interfaces;
@using BitMart.Net.Interfaces; @using Bitstamp.Net.Interfaces;
@using BitMEX.Net.Interfaces; @using Bitunix.Net.Interfaces;
@using BloFin.Net.Interfaces; @using BloFin.Net.Interfaces;
@using Bybit.Net.Interfaces @using Bybit.Net.Interfaces
@using CoinEx.Net.Interfaces
@using CoinW.Net.Interfaces @using CoinW.Net.Interfaces
@using Coinbase.Net.Interfaces @using Coinbase.Net.Interfaces
@using CryptoExchange.Net.Authentication @using CryptoExchange.Net.Authentication
@@ -39,12 +38,11 @@
@inject IBingXOrderBookFactory bingXFactory @inject IBingXOrderBookFactory bingXFactory
@inject IBitfinexOrderBookFactory bitfinexFactory @inject IBitfinexOrderBookFactory bitfinexFactory
@inject IBitgetOrderBookFactory bitgetFactory @inject IBitgetOrderBookFactory bitgetFactory
@inject IBitMartOrderBookFactory bitmartFactory @inject IBitstampOrderBookFactory bitstampFactory
@inject IBitMEXOrderBookFactory bitmexFactory @inject IBitunixOrderBookFactory bitunixFactory
@inject IBloFinOrderBookFactory bloFinFactory @inject IBloFinOrderBookFactory bloFinFactory
@inject IBybitOrderBookFactory bybitFactory @inject IBybitOrderBookFactory bybitFactory
@inject ICoinbaseOrderBookFactory coinbaseFactory @inject ICoinbaseOrderBookFactory coinbaseFactory
@inject ICoinExOrderBookFactory coinExFactory
@inject ICoinWOrderBookFactory coinWFactory @inject ICoinWOrderBookFactory coinWFactory
@inject ICryptoComOrderBookFactory cryptocomFactory @inject ICryptoComOrderBookFactory cryptocomFactory
@inject IDeepCoinOrderBookFactory deepCoinFactory @inject IDeepCoinOrderBookFactory deepCoinFactory
@@ -102,12 +100,11 @@
{ "BingX", bingXFactory.CreateSpot("ETH-BTC") }, { "BingX", bingXFactory.CreateSpot("ETH-BTC") },
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") }, { "Bitfinex", bitfinexFactory.Create("tETHBTC") },
{ "Bitget", bitgetFactory.CreateSpot("ETHBTC") }, { "Bitget", bitgetFactory.CreateSpot("ETHBTC") },
{ "BitMart", bitmartFactory.CreateSpot("ETH_BTC", null) }, { "Bitstamp", bitstampFactory.Create("ETHUSDT", null) },
{ "BitMEX", bitmexFactory.Create("ETH_XBT") }, { "Bitunix", bitunixFactory.CreateFutures("ETHUSDT", null) },
{ "BloFin", bloFinFactory.CreateFutures("ETH-USDT") }, { "BloFin", bloFinFactory.CreateFutures("ETH-USDT") },
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) }, { "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
{ "Coinbase", coinbaseFactory.Create("ETH-BTC", null) }, { "Coinbase", coinbaseFactory.Create("ETH-BTC", null) },
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
{ "CoinW", coinWFactory.CreateSpot("ETH_BTC") }, { "CoinW", coinWFactory.CreateSpot("ETH_BTC") },
{ "CryptoCom", cryptocomFactory.Create("ETH_BTC") }, { "CryptoCom", cryptocomFactory.Create("ETH_BTC") },
{ "GateIo", gateioFactory.CreateSpot("ETH_BTC") }, { "GateIo", gateioFactory.CreateSpot("ETH_BTC") },
+2 -2
View File
@@ -1,7 +1,7 @@
@page "/SpotClient" @page "/SpotClient"
@using CryptoExchange.Net.SharedApis @using CryptoExchange.Net.SharedApis
@using System.Diagnostics @using System.Diagnostics
@inject IEnumerable<ISpotTickerRestClient> restClients @inject IEnumerable<IGetTicker> restClients
<h3>ETH-BTC prices:</h3> <h3>ETH-BTC prices:</h3>
@foreach(var price in _prices.OrderBy(p => p.Key)) @foreach(var price in _prices.OrderBy(p => p.Key))
@@ -15,7 +15,7 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "BTC"); 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)) await foreach (var ticker in ParallelEnumerateAsync(tasks))
{ {
if (ticker.Success) if (ticker.Success)
+5 -9
View File
@@ -6,11 +6,10 @@
@using BingX.Net.Interfaces @using BingX.Net.Interfaces
@using Bitfinex.Net.Interfaces @using Bitfinex.Net.Interfaces
@using Bitget.Net.Interfaces; @using Bitget.Net.Interfaces;
@using BitMEX.Net.Interfaces; @using Bitstamp.Net.Interfaces;
@using BitMart.Net.Interfaces; @using Bitunix.Net.Interfaces;
@using BloFin.Net.Interfaces @using BloFin.Net.Interfaces
@using Bybit.Net.Interfaces @using Bybit.Net.Interfaces
@using CoinEx.Net.Interfaces
@using CoinW.Net.Interfaces @using CoinW.Net.Interfaces
@using Coinbase.Net.Interfaces @using Coinbase.Net.Interfaces
@using CryptoExchange.Net.Interfaces @using CryptoExchange.Net.Interfaces
@@ -39,12 +38,11 @@
@inject IBingXTrackerFactory bingXFactory @inject IBingXTrackerFactory bingXFactory
@inject IBitfinexTrackerFactory bitfinexFactory @inject IBitfinexTrackerFactory bitfinexFactory
@inject IBitgetTrackerFactory bitgetFactory @inject IBitgetTrackerFactory bitgetFactory
@inject IBitMartTrackerFactory bitmartFactory @inject IBitstampTrackerFactory bitstampFactory
@inject IBitMEXTrackerFactory bitmexFactory @inject IBitunixTrackerFactory bitunixFactory
@inject IBloFinTrackerFactory bloFinFactory @inject IBloFinTrackerFactory bloFinFactory
@inject IBybitTrackerFactory bybitFactory @inject IBybitTrackerFactory bybitFactory
@inject ICoinbaseTrackerFactory coinbaseFactory @inject ICoinbaseTrackerFactory coinbaseFactory
@inject ICoinExTrackerFactory coinExFactory
@inject ICoinWTrackerFactory coinWFactory @inject ICoinWTrackerFactory coinWFactory
@inject ICryptoComTrackerFactory cryptocomFactory @inject ICryptoComTrackerFactory cryptocomFactory
@inject IDeepCoinTrackerFactory deepCoinFactory @inject IDeepCoinTrackerFactory deepCoinFactory
@@ -95,12 +93,10 @@
{ bingXFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { bingXFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ bitfinexFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { bitfinexFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ bitgetFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { bitgetFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ bitmartFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { bitstampFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ bitmexFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ bloFinFactory.CreateTradeTracker(futuresSymbol, period: TimeSpan.FromMinutes(5)) }, { bloFinFactory.CreateTradeTracker(futuresSymbol, period: TimeSpan.FromMinutes(5)) },
{ bybitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { bybitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ coinbaseFactory.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)) }, { coinWFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ cryptocomFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { cryptocomFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ deepCoinFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { deepCoinFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
+2 -3
View File
@@ -38,12 +38,11 @@ namespace BlazorClient
services.AddBingX(); services.AddBingX();
services.AddBitfinex(); services.AddBitfinex();
services.AddBitget(); services.AddBitget();
services.AddBitMart(); services.AddBitstamp();
services.AddBitMEX(); services.AddBitunix();
services.AddBloFin(); services.AddBloFin();
services.AddBybit(); services.AddBybit();
services.AddCoinbase(); services.AddCoinbase();
services.AddCoinEx();
services.AddCoinW(); services.AddCoinW();
services.AddCryptoCom(); services.AddCryptoCom();
services.AddDeepCoin(); services.AddDeepCoin();
-3
View File
@@ -13,12 +13,9 @@
@using BingX.Net.Interfaces.Clients; @using BingX.Net.Interfaces.Clients;
@using Bitfinex.Net.Interfaces.Clients; @using Bitfinex.Net.Interfaces.Clients;
@using Bitget.Net.Interfaces.Clients; @using Bitget.Net.Interfaces.Clients;
@using BitMart.Net.Interfaces.Clients;
@using BitMEX.Net.Interfaces.Clients;
@using BloFin.Net.Interfaces.Clients; @using BloFin.Net.Interfaces.Clients;
@using Bybit.Net.Interfaces.Clients; @using Bybit.Net.Interfaces.Clients;
@using Coinbase.Net.Interfaces.Clients; @using Coinbase.Net.Interfaces.Clients;
@using CoinEx.Net.Interfaces.Clients;
@using CoinW.Net.Interfaces.Clients; @using CoinW.Net.Interfaces.Clients;
@using CryptoCom.Net.Interfaces.Clients; @using CryptoCom.Net.Interfaces.Clients;
@using DeepCoin.Net.Interfaces.Clients; @using DeepCoin.Net.Interfaces.Clients;
+2 -2
View File
@@ -8,8 +8,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="12.13.0" /> <PackageReference Include="Binance.Net" Version="13.6.0" />
<PackageReference Include="Bybit.Net" Version="6.13.0" /> <PackageReference Include="Bybit.Net" Version="7.6.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+26 -34
View File
@@ -25,18 +25,22 @@ var exchanges = new Dictionary<string, ExchangeClients>(StringComparer.OrdinalIg
{ {
["binance"] = new ExchangeClients( ["binance"] = new ExchangeClients(
"Binance", "Binance",
binanceRest.SpotApi.SharedClient, binanceRest.SpotApi.SharedApi,
binanceRest.SpotApi.SharedClient, binanceRest.SpotApi.SharedApi,
binanceRest.SpotApi.SharedClient, binanceRest.SpotApi.SharedApi,
binanceRest.SpotApi.SharedClient, binanceRest.SpotApi.SharedApi,
binanceSocket.SpotApi.SharedClient), binanceRest.SpotApi.SharedApi,
binanceRest.SpotApi.SharedApi,
binanceSocket.SpotApi.SharedApi),
["bybit"] = new ExchangeClients( ["bybit"] = new ExchangeClients(
"Bybit", "Bybit",
bybitRest.V5Api.SharedClient, bybitRest.V5Api.SharedApi,
bybitRest.V5Api.SharedClient, bybitRest.V5Api.SharedApi,
bybitRest.V5Api.SharedClient, bybitRest.V5Api.SharedApi,
bybitRest.V5Api.SharedClient, bybitRest.V5Api.SharedApi,
bybitSocket.V5SpotApi.SharedClient) bybitRest.V5Api.SharedApi,
bybitRest.V5Api.SharedApi,
bybitSocket.V5SpotApi.SharedApi)
}; };
PrintHelp(); PrintHelp();
@@ -98,7 +102,7 @@ async Task PrintPricesAsync(string[] args)
var symbol = GetSymbol(args, 1); var symbol = GetSymbol(args, 1);
var tasks = exchanges.Values.Select(async exchange => 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) if (!result.Success)
{ {
Console.WriteLine($"{exchange.Name,-8} error: {result.Error}"); Console.WriteLine($"{exchange.Name,-8} error: {result.Error}");
@@ -171,7 +175,7 @@ async Task PrintOpenOrdersAsync(string[] args)
var exchange = GetExchange(args, 1); var exchange = GetExchange(args, 1);
var symbol = GetSymbol(args, 2); 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) if (!result.Success)
{ {
Console.WriteLine($"Open orders request failed: {result.Error}"); Console.WriteLine($"Open orders request failed: {result.Error}");
@@ -203,23 +207,9 @@ async Task PlaceLimitOrderAsync(string[] args)
SharedQuantity.Base(quantity), SharedQuantity.Base(quantity),
price, price,
SharedTimeInForce.GoodTillCanceled, SharedTimeInForce.GoodTillCanceled,
exchange.OrderRest.GenerateClientOrderId()); exchange.PlaceOrderRest.GenerateClientOrderId());
var validationError = exchange.OrderRest.PlaceSpotOrderOptions.ValidateRequest( var result = await exchange.PlaceOrderRest.PlaceSpotOrderAsync(request);
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);
Console.WriteLine(result.Success Console.WriteLine(result.Success
? $"Order placed. Id: {result.Data.Id}" ? $"Order placed. Id: {result.Data.Id}"
: $"Order failed: {result.Error}"); : $"Order failed: {result.Error}");
@@ -233,7 +223,7 @@ async Task CancelOrderAsync(string[] args)
if (string.IsNullOrWhiteSpace(orderId)) if (string.IsNullOrWhiteSpace(orderId))
throw new ArgumentException("Missing order id. Example: cancel-order binance BTC USDT 123456"); 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 Console.WriteLine(result.Success
? $"Order canceled. Id: {result.Data.Id}" ? $"Order canceled. Id: {result.Data.Id}"
: $"Cancel failed: {result.Error}"); : $"Cancel failed: {result.Error}");
@@ -296,8 +286,10 @@ static void PrintHelp()
internal record ExchangeClients( internal record ExchangeClients(
string Name, string Name,
ISpotTickerRestClient TickerRest, IGetTickerRest TickerRest,
IOrderBookRestClient OrderBookRest, IGetOrderBookRest OrderBookRest,
IBalanceRestClient BalanceRest, IGetBalancesRest BalanceRest,
ISpotOrderRestClient OrderRest, IPlaceSpotOrderRest PlaceOrderRest,
ITickerSocketClient TickerSocket); IGetOpenSpotOrdersRest GetOpenOrdersRest,
ICancelSpotOrderRest CancelOrderRest,
ISubscribeTickerSocket TickerSocket);
+9 -9
View File
@@ -5,13 +5,13 @@ using CryptoExchange.Net.SharedApis;
using OKX.Net.Clients; using OKX.Net.Clients;
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT"); var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
var binanceSpotRestClient = new BinanceRestClient().SpotApi.SharedClient; var binanceSpotRestClient = new BinanceRestClient().SpotApi.SharedApi;
var okxSpotRestClient = new OKXRestClient().UnifiedApi.SharedClient; var okxSpotRestClient = new OKXRestClient().UnifiedApi.SharedApi;
var bitmartSpotRestClient = new BitMartRestClient().SpotApi.SharedClient; var bitmartSpotRestClient = new BitMartRestClient().SpotApi.SharedApi;
var binanceSpotSocketClient = new BinanceSocketClient().SpotApi.SharedClient; var binanceSpotSocketClient = new BinanceSocketClient().SpotApi.SharedApi;
var okxSpotSocketClient = new OKXSocketClient().UnifiedApi.SharedClient; var okxSpotSocketClient = new OKXSocketClient().UnifiedApi.SharedApi;
var bitmartSpotSocketClient = new BitMartSocketClient().SpotApi.SharedClient; var bitmartSpotSocketClient = new BitMartSocketClient().SpotApi.SharedApi;
await GetLastTradePriceAsync(binanceSpotRestClient, symbol); await GetLastTradePriceAsync(binanceSpotRestClient, symbol);
await GetLastTradePriceAsync(okxSpotRestClient, symbol); await GetLastTradePriceAsync(okxSpotRestClient, symbol);
@@ -31,9 +31,9 @@ Console.ReadLine();
foreach (var subscription in subscriptions) foreach (var subscription in subscriptions)
await subscription.CloseAsync(); 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) if (!result.Success)
{ {
Console.WriteLine($"Failed to get ticker: {result.Error}"); 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}"); 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 => var result = await client.SubscribeToTickerUpdatesAsync(new SubscribeTickerRequest(symbol), update =>
{ {
+3 -3
View File
@@ -8,9 +8,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="12.13.0" /> <PackageReference Include="Binance.Net" Version="13.6.0" />
<PackageReference Include="BitMart.Net" Version="3.11.2" /> <PackageReference Include="BitMart.Net" Version="4.5.0" />
<PackageReference Include="JK.OKX.Net" Version="4.14.0" /> <PackageReference Include="JK.OKX.Net" Version="5.5.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+28
View File
@@ -39,6 +39,7 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|![Bitget](https://raw.githubusercontent.com/JKorf/Bitget.Net/refs/heads/main/Bitget.Net/Icon/icon.png)|Bitget|CEX|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Bitget.Net)|[Link](https://partner.bitget.com/bg/1qlf6pj1)|20%| |![Bitget](https://raw.githubusercontent.com/JKorf/Bitget.Net/refs/heads/main/Bitget.Net/Icon/icon.png)|Bitget|CEX|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Bitget.Net)|[Link](https://partner.bitget.com/bg/1qlf6pj1)|20%|
|![BitMart](https://raw.githubusercontent.com/JKorf/BitMart.Net/refs/heads/main/BitMart.Net/Icon/icon.png)|BitMart|CEX|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[![Nuget version](https://img.shields.io/nuget/v/BitMart.net.svg?style=flat-square)](https://www.nuget.org/packages/BitMart.Net)|[Link](https://www.bitmart.com/invite/JKorfAPI/en-US)|30%| |![BitMart](https://raw.githubusercontent.com/JKorf/BitMart.Net/refs/heads/main/BitMart.Net/Icon/icon.png)|BitMart|CEX|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[![Nuget version](https://img.shields.io/nuget/v/BitMart.net.svg?style=flat-square)](https://www.nuget.org/packages/BitMart.Net)|[Link](https://www.bitmart.com/invite/JKorfAPI/en-US)|30%|
|![Bitstamp](https://raw.githubusercontent.com/JKorf/Bitstamp.Net/refs/heads/main/Bitstamp.Net/Icon/icon.png)|Bitstamp|CEX|[JKorf/Bitstamp.Net](https://github.com/JKorf/Bitstamp.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitstamp.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitstamp.Net)|-|-| |![Bitstamp](https://raw.githubusercontent.com/JKorf/Bitstamp.Net/refs/heads/main/Bitstamp.Net/Icon/icon.png)|Bitstamp|CEX|[JKorf/Bitstamp.Net](https://github.com/JKorf/Bitstamp.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitstamp.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitstamp.Net)|-|-|
|![Bitunix](https://raw.githubusercontent.com/JKorf/Bitunix.Net/refs/heads/main/Bitunix.Net/Icon/icon.png)|Bitunix|CEX|[JKorf/Bitunix.Net](https://github.com/JKorf/Bitunix.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitunix.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitunix.Net)|-|-|
|![BloFin](https://raw.githubusercontent.com/JKorf/BloFin.Net/refs/heads/main/BloFin.Net/Icon/icon.png)|BloFin|CEX|[JKorf/BloFin.Net](https://github.com/JKorf/BloFin.Net)|[![Nuget version](https://img.shields.io/nuget/v/BloFin.net.svg?style=flat-square)](https://www.nuget.org/packages/BloFin.Net)|-|-| |![BloFin](https://raw.githubusercontent.com/JKorf/BloFin.Net/refs/heads/main/BloFin.Net/Icon/icon.png)|BloFin|CEX|[JKorf/BloFin.Net](https://github.com/JKorf/BloFin.Net)|[![Nuget version](https://img.shields.io/nuget/v/BloFin.net.svg?style=flat-square)](https://www.nuget.org/packages/BloFin.Net)|-|-|
|![Bybit](https://raw.githubusercontent.com/JKorf/Bybit.Net/refs/heads/main/ByBit.Net/Icon/icon.png)|Bybit|CEX|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)|[Link](https://partner.bybit.com/b/jkorf)|-| |![Bybit](https://raw.githubusercontent.com/JKorf/Bybit.Net/refs/heads/main/ByBit.Net/Icon/icon.png)|Bybit|CEX|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)|[Link](https://partner.bybit.com/b/jkorf)|-|
|![Coinbase](https://raw.githubusercontent.com/JKorf/Coinbase.Net/refs/heads/main/Coinbase.Net/Icon/icon.png)|Coinbase|CEX|[JKorf/Coinbase.Net](https://github.com/JKorf/Coinbase.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Coinbase.Net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Coinbase.Net)|[Link](https://advanced.coinbase.com/join/T6H54H8)|-| |![Coinbase](https://raw.githubusercontent.com/JKorf/Coinbase.Net/refs/heads/main/Coinbase.Net/Icon/icon.png)|Coinbase|CEX|[JKorf/Coinbase.Net](https://github.com/JKorf/Coinbase.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Coinbase.Net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Coinbase.Net)|[Link](https://advanced.coinbase.com/join/T6H54H8)|-|
@@ -127,6 +128,33 @@ Various:
* PlatformInfo now required support environment names in the constructor * PlatformInfo now required support environment names in the constructor
## Release notes ## 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 * Version 13.0.0 - 23 Sep 2026
* Shared APIs * Shared APIs
* Added Shared API V2 with fine-grained capability interfaces for individual REST requests, WebSocket requests and subscriptions * Added Shared API V2 with fine-grained capability interfaces for individual REST requests, WebSocket requests and subscriptions
+2 -2
View File
@@ -1,8 +1,8 @@
# CryptoExchange.Net — full AI context # CryptoExchange.Net — full AI context
Library: CryptoExchange.Net Library: CryptoExchange.Net
Library version: 13.0.0 Library version: 13.2.2
CryptoExchange.Net package version: 13.0.0 CryptoExchange.Net package version: 13.2.2
Language: C#/.NET Language: C#/.NET
Targets: netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0 Targets: netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0
+1 -1
View File
@@ -2,7 +2,7 @@
> Base C#/.NET library used by 28+ cryptocurrency exchange clients. It standardizes REST, WebSocket, authentication, rate limiting, results, and exchange-agnostic Shared APIs. > 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 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.0.0. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, and net10.0; Native AOT is 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.
## Shared API V2 ## Shared API V2