mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2026-04-12 16:13:12 +00:00
Compare commits
4 Commits
33c0fb26a7
...
875696a73a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
875696a73a | ||
|
|
90c61715d0 | ||
|
|
12ca45050f | ||
|
|
eaf092a334 |
@ -56,7 +56,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
var request = new Mock<IRequest>();
|
||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
|
||||
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<Encoding>(), It.IsAny<string>())).Callback(new Action<string, Encoding, string>((content, encoding, type) => { request.Setup(r => r.Content).Returns(content); }));
|
||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
|
||||
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
||||
|
||||
|
||||
147
CryptoExchange.Net/Authentication/Signing/CeAbiEncoder.cs
Normal file
147
CryptoExchange.Net/Authentication/Signing/CeAbiEncoder.cs
Normal file
@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication.Signing
|
||||
{
|
||||
/// <summary>
|
||||
/// ABI encoding
|
||||
/// </summary>
|
||||
public static class CeAbiEncoder
|
||||
{
|
||||
/// <summary>
|
||||
/// ABI encode string as Sha3Keccack hashed byte value
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeString(string value)
|
||||
{
|
||||
var abiValueEncoded = CeSha3Keccack.CalculateHash(Encoding.UTF8.GetBytes(value));
|
||||
return abiValueEncoded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode bool value as uint256 with 1 for true and 0 for false, as per ABI specification
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeBool(bool value)
|
||||
=> AbiValueEncodeInt((byte)(value ? 1 : 0));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode byte value as uint256, as per ABI specification
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeInt(byte value)
|
||||
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode short value as int256, as per ABI specification
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeInt(short value)
|
||||
=> AbiValueEncodeBigInteger(true, new BigInteger(value));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode int value as int256, as per ABI specification
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static byte[] AbiValueEncodeInt(int value)
|
||||
=> AbiValueEncodeBigInteger(true, new BigInteger(value));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode long value as int256, as per ABI specification
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeInt(long value)
|
||||
=> AbiValueEncodeBigInteger(true, new BigInteger(value));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode ushort value as uint256, as per ABI specification
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeInt(ushort value)
|
||||
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode uint value as uint256, as per ABI specification
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static byte[] AbiValueEncodeInt(uint value)
|
||||
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode ulong value as uint256, as per ABI specification
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static byte[] AbiValueEncodeInt(ulong value)
|
||||
=> AbiValueEncodeBigInteger(false, new BigInteger(value));
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode big integer value as int256 or uint256, as per ABI specification
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeBigInteger(bool signed, BigInteger value)
|
||||
{
|
||||
var result = new byte[32];
|
||||
if (signed && value < 0)
|
||||
{
|
||||
// Pad with FF
|
||||
for (int i = 0; i < result.Length; i++)
|
||||
{
|
||||
result[i] = 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
var t = value.ToByteArray();
|
||||
if (t.Length == 33)
|
||||
{
|
||||
// Strip last byte
|
||||
var strip1 = new byte[32];
|
||||
Array.Copy(t, 0, strip1, 0, 32);
|
||||
t = strip1;
|
||||
}
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
t = t.AsEnumerable().Reverse().ToArray();
|
||||
|
||||
t.CopyTo(result, result.Length - t.Length);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode address value as uint256, as per ABI specification
|
||||
/// </summary>
|
||||
public static byte[] AbiValueEncodeAddress(string value)
|
||||
{
|
||||
var result = new byte[32];
|
||||
var h = value.HexStringToBytes();
|
||||
h.CopyTo(result, result.Length - h.Length);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode hex string value as bytes32, as per ABI specification. The hex string is expected to be a 0x prefixed string, and the resulting bytes will be right aligned in the 32 bytes result, with leading zeros if the hex string is shorter than 32 bytes. If the hex string is longer than 32 bytes, an exception will be thrown.
|
||||
/// </summary>
|
||||
/// <param name="length"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static byte[] AbiValueEncodeHexBytes(int length, string value)
|
||||
=> AbiValueEncodeBytes(value.Length, value.HexStringToBytes());
|
||||
|
||||
/// <summary>
|
||||
/// ABI encode byte array value as bytes32, as per ABI specification. The resulting bytes will be right aligned in the 32 bytes result, with leading zeros if the byte array is shorter than 32 bytes. If the byte array is longer than 32 bytes, an exception will be thrown.
|
||||
/// </summary>
|
||||
/// <param name="length"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public static byte[] AbiValueEncodeBytes(int length, byte[] value)
|
||||
{
|
||||
if (length != 32)
|
||||
throw new Exception("Only 32 bytes size supported");
|
||||
|
||||
if (value.Length == 32)
|
||||
return value;
|
||||
|
||||
var result = new byte[32];
|
||||
value.CopyTo(result, result.Length - value.Length);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,312 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication.Signing
|
||||
{
|
||||
/// <summary>
|
||||
/// EIP712 Typed Data Encoder
|
||||
/// </summary>
|
||||
public static class CeEip712TypedDataEncoder
|
||||
{
|
||||
/// <summary>
|
||||
/// Encode EIP712 typed data according to the specification, with the provided primary type, domain fields and message fields.
|
||||
/// The resulting byte array is the 0x19 0x01 prefix followed by the hash of the domain and the hash of the message, which can be signed with ECDSA secp256k1 to produce a signature that can be verified on chain with EIP712.
|
||||
/// Note that this implementation does not support all possible EIP712 types, but it should cover most common use cases
|
||||
/// </summary>
|
||||
public static byte[] EncodeEip721(
|
||||
string primaryType,
|
||||
IEnumerable<(string Name, string Type, object Value)> domainFields,
|
||||
IEnumerable<(string Name, string Type, object Value)> messageFields)
|
||||
{
|
||||
var data = new CeTypedDataRaw()
|
||||
{
|
||||
PrimaryType = primaryType,
|
||||
DomainRawValues = domainFields.Select(x => new CeMemberValue
|
||||
{
|
||||
TypeName = x.Type,
|
||||
Value = x.Value,
|
||||
}).ToArray(),
|
||||
|
||||
Message = messageFields.Select(x => new CeMemberValue
|
||||
{
|
||||
TypeName = x.Type,
|
||||
Value = x.Value,
|
||||
}).ToArray(),
|
||||
Types = new Dictionary<string, CeMemberDescription[]>
|
||||
{
|
||||
{
|
||||
"EIP712Domain",
|
||||
domainFields.Select(x => new CeMemberDescription
|
||||
{
|
||||
Name = x.Name,
|
||||
Type = x.Type
|
||||
}).ToArray()
|
||||
},
|
||||
{
|
||||
primaryType,
|
||||
messageFields.Select(x => new CeMemberDescription
|
||||
{
|
||||
Name = x.Name,
|
||||
Type = x.Type
|
||||
}).ToArray()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return EncodeTypedDataRaw(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encode EIP712 typed data according to the specification, with the provided primary type, domain fields and message fields.
|
||||
/// The resulting byte array is the 0x19 0x01 prefix followed by the hash of the domain and the hash of the message, which can be signed with ECDSA secp256k1 to produce a signature that can be verified on chain with EIP712.
|
||||
/// Note that this implementation does not support all possible EIP712 types, but it should cover most common use cases
|
||||
/// </summary>
|
||||
public static byte[] EncodeTypedDataRaw(CeTypedDataRaw typedData)
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using var writer = new BinaryWriter(memoryStream);
|
||||
|
||||
// Write 0x19 0x01 prefix
|
||||
writer.Write((byte)0x19);
|
||||
writer.Write((byte)0x01);
|
||||
|
||||
// Write domain
|
||||
writer.Write(HashStruct(typedData.Types, "EIP712Domain", typedData.DomainRawValues));
|
||||
|
||||
// Write message
|
||||
writer.Write(HashStruct(typedData.Types, typedData.PrimaryType, typedData.Message));
|
||||
|
||||
writer.Flush();
|
||||
var result = memoryStream.ToArray();
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
private static byte[] HashStruct(IDictionary<string, CeMemberDescription[]> types, string primaryType, IEnumerable<CeMemberValue> message)
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
var writer = new BinaryWriter(memoryStream);
|
||||
|
||||
// Encode the type header
|
||||
EncodeType(writer, types, primaryType);
|
||||
|
||||
// Encode the data
|
||||
EncodeData(writer, types, message);
|
||||
|
||||
writer.Flush();
|
||||
return CeSha3Keccack.CalculateHash(memoryStream.ToArray());
|
||||
|
||||
}
|
||||
|
||||
private static void EncodeData(BinaryWriter writer, IDictionary<string, CeMemberDescription[]> types, IEnumerable<CeMemberValue> memberValues)
|
||||
{
|
||||
foreach (var memberValue in memberValues)
|
||||
{
|
||||
switch (memberValue.TypeName)
|
||||
{
|
||||
case var refType when IsReferenceType(refType):
|
||||
writer.Write(HashStruct(types, memberValue.TypeName, (IEnumerable<CeMemberValue>)memberValue.Value));
|
||||
break;
|
||||
|
||||
case "string":
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeString((string)memberValue.Value));
|
||||
break;
|
||||
|
||||
case "bool":
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeBool((bool)memberValue.Value));
|
||||
break;
|
||||
|
||||
case "address":
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeAddress((string)memberValue.Value));
|
||||
break;
|
||||
|
||||
default:
|
||||
if (memberValue.TypeName.Contains("["))
|
||||
{
|
||||
var items = (IList)memberValue.Value;
|
||||
var itemsMemberValues = new List<CeMemberValue>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
itemsMemberValues.Add(new CeMemberValue()
|
||||
{
|
||||
TypeName = memberValue.TypeName.Substring(0, memberValue.TypeName.LastIndexOf("[")),
|
||||
Value = item
|
||||
});
|
||||
}
|
||||
|
||||
var memoryStream = new MemoryStream();
|
||||
var writerItem = new BinaryWriter(memoryStream);
|
||||
|
||||
EncodeData(writerItem, types, itemsMemberValues);
|
||||
writerItem.Flush();
|
||||
writer.Write(CeSha3Keccack.CalculateHash(memoryStream.ToArray()));
|
||||
}
|
||||
else if (memberValue.TypeName.StartsWith("int") || memberValue.TypeName.StartsWith("uint"))
|
||||
{
|
||||
if (memberValue.Value is string v)
|
||||
{
|
||||
if (!BigInteger.TryParse(v, out BigInteger parsedOutput))
|
||||
throw new Exception("");
|
||||
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeBigInteger(memberValue.TypeName[0] != 'u', parsedOutput));
|
||||
}
|
||||
else if (memberValue.Value is byte b)
|
||||
{
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeInt(b));
|
||||
}
|
||||
else if (memberValue.Value is short s)
|
||||
{
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeInt(s));
|
||||
}
|
||||
else if (memberValue.Value is int i)
|
||||
{
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeInt(i));
|
||||
}
|
||||
else if (memberValue.Value is long l)
|
||||
{
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeInt(l));
|
||||
}
|
||||
else if (memberValue.Value is ushort us)
|
||||
{
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeInt(us));
|
||||
}
|
||||
else if (memberValue.Value is uint ui)
|
||||
{
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeInt(ui));
|
||||
}
|
||||
else if (memberValue.Value is ulong ul)
|
||||
{
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeInt(ul));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
}
|
||||
else if (memberValue.TypeName.StartsWith("bytes"))
|
||||
{
|
||||
// Applicable?
|
||||
//if (memberValue.Value is string v)
|
||||
// writer.Write(AbiEncoder.AbiValueEncodeHexBytes(v));
|
||||
//else if (memberValue.Value is byte[] b)
|
||||
// writer.Write(AbiEncoder.AbiValueEncodeBytes(b));
|
||||
//else
|
||||
// throw new Exception("Unknown byte value type");
|
||||
|
||||
var length = memberValue.TypeName.Length == 5 ? 32 : int.Parse(memberValue.TypeName.Substring(5));
|
||||
writer.Write(CeAbiEncoder.AbiValueEncodeBytes(length, (byte[])memberValue.Value));
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EncodeType(BinaryWriter writer, IDictionary<string, CeMemberDescription[]> types, string typeName)
|
||||
{
|
||||
var encodedTypes = EncodeTypes(types, typeName);
|
||||
var encodedPrimaryType = encodedTypes.Single(x => x.Key == typeName);
|
||||
var encodedReferenceTypes = encodedTypes.Where(x => x.Key != typeName).OrderBy(x => x.Key).Select(x => x.Value);
|
||||
var fullyEncodedType = encodedPrimaryType.Value + string.Join(string.Empty, encodedReferenceTypes.ToArray());
|
||||
|
||||
writer.Write(CeSha3Keccack.CalculateHash(Encoding.UTF8.GetBytes(fullyEncodedType)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a list of type => type(parameters), for example:<br />
|
||||
/// { IP712Domain, EIP712Domain(string name,string version,uint256 chainId,address verifyingContract) }
|
||||
/// </summary>
|
||||
private static IList<KeyValuePair<string, string>> EncodeTypes(IDictionary<string, CeMemberDescription[]> types, string currentTypeName)
|
||||
{
|
||||
var currentTypeMembers = types[currentTypeName];
|
||||
var currentTypeMembersEncoded = currentTypeMembers.Select(x => x.Type + " " + x.Name);
|
||||
var result = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>(currentTypeName, currentTypeName + "(" + string.Join(",", currentTypeMembersEncoded.ToArray()) + ")")
|
||||
};
|
||||
|
||||
result.AddRange(currentTypeMembers.Select(x => x.Type.Contains("[") ? x.Type.Substring(0, x.Type.IndexOf("[")) : x.Type)
|
||||
.Distinct()
|
||||
.Where(IsReferenceType)
|
||||
.SelectMany(x => EncodeTypes(types, x)));
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static bool IsReferenceType(string typeName)
|
||||
{
|
||||
switch (typeName)
|
||||
{
|
||||
case var bytes when new Regex("bytes\\d+").IsMatch(bytes):
|
||||
case var @uint when new Regex("uint\\d+").IsMatch(@uint):
|
||||
case var @int when new Regex("int\\d+").IsMatch(@int):
|
||||
case "bytes":
|
||||
case "string":
|
||||
case "bool":
|
||||
case "address":
|
||||
case var array when array.Contains("["):
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Member description
|
||||
/// </summary>
|
||||
public class CeMemberDescription
|
||||
{
|
||||
/// <summary>
|
||||
/// Name
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Type
|
||||
/// </summary>
|
||||
public string Type { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Member value
|
||||
/// </summary>
|
||||
public class CeMemberValue
|
||||
{
|
||||
/// <summary>
|
||||
/// Type name
|
||||
/// </summary>
|
||||
public string TypeName { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Value
|
||||
/// </summary>
|
||||
public object Value { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Typed data raw, used for encoding EIP712 typed data with the provided primary type, domain fields and message fields.
|
||||
/// </summary>
|
||||
public class CeTypedDataRaw
|
||||
{
|
||||
/// <summary>
|
||||
/// Type dictionary
|
||||
/// </summary>
|
||||
public IDictionary<string, CeMemberDescription[]> Types { get; set; } = new Dictionary<string, CeMemberDescription[]>();
|
||||
/// <summary>
|
||||
/// Primary type
|
||||
/// </summary>
|
||||
public string PrimaryType { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Message values
|
||||
/// </summary>
|
||||
public CeMemberValue[] Message { get; set; } = [];
|
||||
/// <summary>
|
||||
/// Domain values
|
||||
/// </summary>
|
||||
public CeMemberValue[] DomainRawValues { get; set; } = [];
|
||||
}
|
||||
}
|
||||
385
CryptoExchange.Net/Authentication/Signing/CeSha3Keccack.cs
Normal file
385
CryptoExchange.Net/Authentication/Signing/CeSha3Keccack.cs
Normal file
@ -0,0 +1,385 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication.Signing
|
||||
{
|
||||
/// <summary>
|
||||
/// Sha3 Keccack hashing, as per Ethereum specification, with 256 bit output
|
||||
/// </summary>
|
||||
public class CeSha3Keccack
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculate the Keccack256 hash of the provided data, as per Ethereum specification
|
||||
/// </summary>
|
||||
public static byte[] CalculateHash(byte[] data)
|
||||
{
|
||||
var digest = new CeKeccakDigest256();
|
||||
var output = new byte[digest.GetDigestSize()];
|
||||
digest.BlockUpdate(data, data.Length);
|
||||
digest.DoFinal(output, 0);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
internal class CeKeccakDigest256
|
||||
{
|
||||
private static readonly ulong[] _keccakRoundConstants = KeccakInitializeRoundConstants();
|
||||
private static readonly int[] _keccakRhoOffsets = KeccakInitializeRhoOffsets();
|
||||
|
||||
private readonly int _rate;
|
||||
private const int _stateLength = 1600 / 8;
|
||||
private readonly ulong[] _state = new ulong[_stateLength / 8];
|
||||
private readonly byte[] _dataQueue = new byte[1536 / 8];
|
||||
private int _bitsInQueue;
|
||||
private int _fixedOutputLength;
|
||||
private bool _squeezing;
|
||||
private int _bitsAvailableForSqueezing;
|
||||
|
||||
public CeKeccakDigest256()
|
||||
{
|
||||
_rate = 1600 - (256 << 1);
|
||||
_bitsInQueue = 0;
|
||||
_squeezing = false;
|
||||
_bitsAvailableForSqueezing = 0;
|
||||
_fixedOutputLength = 1600 - _rate >> 1;
|
||||
}
|
||||
|
||||
internal void BlockUpdate(byte[] data, int length)
|
||||
{
|
||||
int bytesInQueue = _bitsInQueue >> 3;
|
||||
int rateBytes = _rate >> 3;
|
||||
|
||||
int count = 0;
|
||||
while (count < length)
|
||||
{
|
||||
if (bytesInQueue == 0 && count <= length - rateBytes)
|
||||
{
|
||||
do
|
||||
{
|
||||
KeccakAbsorb(data, count);
|
||||
count += rateBytes;
|
||||
} while (count <= length - rateBytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
int partialBlock = Math.Min(rateBytes - bytesInQueue, length - count);
|
||||
Array.Copy(data, count, _dataQueue, bytesInQueue, partialBlock);
|
||||
|
||||
bytesInQueue += partialBlock;
|
||||
count += partialBlock;
|
||||
|
||||
if (bytesInQueue == rateBytes)
|
||||
{
|
||||
KeccakAbsorb(_dataQueue, 0);
|
||||
bytesInQueue = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_bitsInQueue = bytesInQueue << 3;
|
||||
}
|
||||
|
||||
internal void DoFinal(byte[] output, int outOff)
|
||||
{
|
||||
Squeeze(output, outOff, _fixedOutputLength >> 3);
|
||||
}
|
||||
|
||||
internal int GetDigestSize() => _fixedOutputLength >> 3;
|
||||
|
||||
protected void Squeeze(byte[] output, int off, int len)
|
||||
{
|
||||
if (!_squeezing)
|
||||
PadAndSwitchToSqueezingPhase();
|
||||
|
||||
long outputLength = (long)len << 3;
|
||||
long i = 0;
|
||||
while (i < outputLength)
|
||||
{
|
||||
if (_bitsAvailableForSqueezing == 0)
|
||||
{
|
||||
KeccakPermutation();
|
||||
KeccakExtract();
|
||||
_bitsAvailableForSqueezing = _rate;
|
||||
}
|
||||
|
||||
int partialBlock = (int)Math.Min(_bitsAvailableForSqueezing, outputLength - i);
|
||||
Array.Copy(_dataQueue, _rate - _bitsAvailableForSqueezing >> 3, output, off + (int)(i >> 3),
|
||||
partialBlock >> 3);
|
||||
_bitsAvailableForSqueezing -= partialBlock;
|
||||
i += partialBlock;
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong[] KeccakInitializeRoundConstants()
|
||||
{
|
||||
ulong[] keccakRoundConstants = new ulong[24];
|
||||
byte LFSRState = 0x01;
|
||||
|
||||
for (int i = 0; i < 24; i++)
|
||||
{
|
||||
keccakRoundConstants[i] = 0;
|
||||
for (int j = 0; j < 7; j++)
|
||||
{
|
||||
int bitPosition = (1 << j) - 1;
|
||||
|
||||
// LFSR86540
|
||||
|
||||
bool loBit = (LFSRState & 0x01) != 0;
|
||||
if (loBit)
|
||||
keccakRoundConstants[i] ^= 1UL << bitPosition;
|
||||
|
||||
bool hiBit = (LFSRState & 0x80) != 0;
|
||||
LFSRState <<= 1;
|
||||
if (hiBit)
|
||||
LFSRState ^= 0x71;
|
||||
}
|
||||
}
|
||||
|
||||
return keccakRoundConstants;
|
||||
}
|
||||
private static int[] KeccakInitializeRhoOffsets()
|
||||
{
|
||||
int[] keccakRhoOffsets = new int[25];
|
||||
int x, y, t, newX, newY;
|
||||
|
||||
int rhoOffset = 0;
|
||||
keccakRhoOffsets[0] = rhoOffset;
|
||||
x = 1;
|
||||
y = 0;
|
||||
for (t = 1; t < 25; t++)
|
||||
{
|
||||
rhoOffset = rhoOffset + t & 63;
|
||||
keccakRhoOffsets[x % 5 + 5 * (y % 5)] = rhoOffset;
|
||||
newX = (0 * x + 1 * y) % 5;
|
||||
newY = (2 * x + 3 * y) % 5;
|
||||
x = newX;
|
||||
y = newY;
|
||||
}
|
||||
|
||||
return keccakRhoOffsets;
|
||||
}
|
||||
|
||||
private void KeccakAbsorb(byte[] data, int off)
|
||||
{
|
||||
int count = _rate >> 6;
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
_state[i] ^= Pack.LeToUInt64(data, off);
|
||||
off += 8;
|
||||
}
|
||||
|
||||
KeccakPermutation();
|
||||
}
|
||||
|
||||
private void KeccakPermutation()
|
||||
{
|
||||
for (int i = 0; i < 24; i++)
|
||||
{
|
||||
Theta(_state);
|
||||
Rho(_state);
|
||||
Pi(_state);
|
||||
Chi(_state);
|
||||
Iota(_state, i);
|
||||
}
|
||||
}
|
||||
private static ulong LeftRotate(ulong v, int r)
|
||||
{
|
||||
return v << r | v >> -r;
|
||||
}
|
||||
|
||||
private static void Theta(ulong[] A)
|
||||
{
|
||||
ulong C0 = A[0 + 0] ^ A[0 + 5] ^ A[0 + 10] ^ A[0 + 15] ^ A[0 + 20];
|
||||
ulong C1 = A[1 + 0] ^ A[1 + 5] ^ A[1 + 10] ^ A[1 + 15] ^ A[1 + 20];
|
||||
ulong C2 = A[2 + 0] ^ A[2 + 5] ^ A[2 + 10] ^ A[2 + 15] ^ A[2 + 20];
|
||||
ulong C3 = A[3 + 0] ^ A[3 + 5] ^ A[3 + 10] ^ A[3 + 15] ^ A[3 + 20];
|
||||
ulong C4 = A[4 + 0] ^ A[4 + 5] ^ A[4 + 10] ^ A[4 + 15] ^ A[4 + 20];
|
||||
|
||||
ulong dX = LeftRotate(C1, 1) ^ C4;
|
||||
|
||||
A[0] ^= dX;
|
||||
A[5] ^= dX;
|
||||
A[10] ^= dX;
|
||||
A[15] ^= dX;
|
||||
A[20] ^= dX;
|
||||
|
||||
dX = LeftRotate(C2, 1) ^ C0;
|
||||
|
||||
A[1] ^= dX;
|
||||
A[6] ^= dX;
|
||||
A[11] ^= dX;
|
||||
A[16] ^= dX;
|
||||
A[21] ^= dX;
|
||||
|
||||
dX = LeftRotate(C3, 1) ^ C1;
|
||||
|
||||
A[2] ^= dX;
|
||||
A[7] ^= dX;
|
||||
A[12] ^= dX;
|
||||
A[17] ^= dX;
|
||||
A[22] ^= dX;
|
||||
|
||||
dX = LeftRotate(C4, 1) ^ C2;
|
||||
|
||||
A[3] ^= dX;
|
||||
A[8] ^= dX;
|
||||
A[13] ^= dX;
|
||||
A[18] ^= dX;
|
||||
A[23] ^= dX;
|
||||
|
||||
dX = LeftRotate(C0, 1) ^ C3;
|
||||
|
||||
A[4] ^= dX;
|
||||
A[9] ^= dX;
|
||||
A[14] ^= dX;
|
||||
A[19] ^= dX;
|
||||
A[24] ^= dX;
|
||||
}
|
||||
|
||||
private static void Rho(ulong[] A)
|
||||
{
|
||||
// KeccakRhoOffsets[0] == 0
|
||||
for (int x = 1; x < 25; x++)
|
||||
{
|
||||
A[x] = LeftRotate(A[x], _keccakRhoOffsets[x]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Pi(ulong[] A)
|
||||
{
|
||||
ulong a1 = A[1];
|
||||
A[1] = A[6];
|
||||
A[6] = A[9];
|
||||
A[9] = A[22];
|
||||
A[22] = A[14];
|
||||
A[14] = A[20];
|
||||
A[20] = A[2];
|
||||
A[2] = A[12];
|
||||
A[12] = A[13];
|
||||
A[13] = A[19];
|
||||
A[19] = A[23];
|
||||
A[23] = A[15];
|
||||
A[15] = A[4];
|
||||
A[4] = A[24];
|
||||
A[24] = A[21];
|
||||
A[21] = A[8];
|
||||
A[8] = A[16];
|
||||
A[16] = A[5];
|
||||
A[5] = A[3];
|
||||
A[3] = A[18];
|
||||
A[18] = A[17];
|
||||
A[17] = A[11];
|
||||
A[11] = A[7];
|
||||
A[7] = A[10];
|
||||
A[10] = a1;
|
||||
}
|
||||
|
||||
private static void Chi(ulong[] A)
|
||||
{
|
||||
ulong chiC0, chiC1, chiC2, chiC3, chiC4;
|
||||
|
||||
for (int yBy5 = 0; yBy5 < 25; yBy5 += 5)
|
||||
{
|
||||
chiC0 = A[0 + yBy5] ^ ~A[(0 + 1) % 5 + yBy5] & A[(0 + 2) % 5 + yBy5];
|
||||
chiC1 = A[1 + yBy5] ^ ~A[(1 + 1) % 5 + yBy5] & A[(1 + 2) % 5 + yBy5];
|
||||
chiC2 = A[2 + yBy5] ^ ~A[(2 + 1) % 5 + yBy5] & A[(2 + 2) % 5 + yBy5];
|
||||
chiC3 = A[3 + yBy5] ^ ~A[(3 + 1) % 5 + yBy5] & A[(3 + 2) % 5 + yBy5];
|
||||
chiC4 = A[4 + yBy5] ^ ~A[(4 + 1) % 5 + yBy5] & A[(4 + 2) % 5 + yBy5];
|
||||
|
||||
A[0 + yBy5] = chiC0;
|
||||
A[1 + yBy5] = chiC1;
|
||||
A[2 + yBy5] = chiC2;
|
||||
A[3 + yBy5] = chiC3;
|
||||
A[4 + yBy5] = chiC4;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Iota(ulong[] A, int indexRound)
|
||||
{
|
||||
A[0] ^= _keccakRoundConstants[indexRound];
|
||||
}
|
||||
|
||||
private void PadAndSwitchToSqueezingPhase()
|
||||
{
|
||||
Debug.Assert(_bitsInQueue < _rate);
|
||||
|
||||
_dataQueue[_bitsInQueue >> 3] |= (byte)(1U << (_bitsInQueue & 7));
|
||||
|
||||
if (++_bitsInQueue == _rate)
|
||||
{
|
||||
KeccakAbsorb(_dataQueue, 0);
|
||||
_bitsInQueue = 0;
|
||||
}
|
||||
|
||||
{
|
||||
int full = _bitsInQueue >> 6, partial = _bitsInQueue & 63;
|
||||
int off = 0;
|
||||
for (int i = 0; i < full; ++i)
|
||||
{
|
||||
_state[i] ^= Pack.LeToUInt64(_dataQueue, off);
|
||||
off += 8;
|
||||
}
|
||||
|
||||
if (partial > 0)
|
||||
{
|
||||
ulong mask = (1UL << partial) - 1UL;
|
||||
_state[full] ^= Pack.LeToUInt64(_dataQueue, off) & mask;
|
||||
}
|
||||
|
||||
_state[_rate - 1 >> 6] ^= 1UL << 63;
|
||||
}
|
||||
|
||||
KeccakPermutation();
|
||||
KeccakExtract();
|
||||
_bitsAvailableForSqueezing = _rate;
|
||||
|
||||
_bitsInQueue = 0;
|
||||
_squeezing = true;
|
||||
}
|
||||
private void KeccakExtract()
|
||||
{
|
||||
Pack.UInt64ToLe(_state, 0, _rate >> 6, _dataQueue, 0);
|
||||
}
|
||||
|
||||
static class Pack
|
||||
{
|
||||
internal static ulong LeToUInt64(byte[] bs, int off)
|
||||
{
|
||||
uint lo = LeToUInt32(bs, off);
|
||||
uint hi = LeToUInt32(bs, off + 4);
|
||||
return (ulong)hi << 32 | lo;
|
||||
}
|
||||
internal static uint LeToUInt32(byte[] bs, int off)
|
||||
{
|
||||
return bs[off]
|
||||
| (uint)bs[off + 1] << 8
|
||||
| (uint)bs[off + 2] << 16
|
||||
| (uint)bs[off + 3] << 24;
|
||||
}
|
||||
|
||||
internal static void UInt64ToLe(ulong[] ns, int nsOff, int nsLen, byte[] bs, int bsOff)
|
||||
{
|
||||
for (int i = 0; i < nsLen; ++i)
|
||||
{
|
||||
UInt64ToLe(ns[nsOff + i], bs, bsOff);
|
||||
bsOff += 8;
|
||||
}
|
||||
}
|
||||
internal static void UInt64ToLe(ulong n, byte[] bs, int off)
|
||||
{
|
||||
UInt32ToLe((uint)n, bs, off);
|
||||
UInt32ToLe((uint)(n >> 32), bs, off + 4);
|
||||
}
|
||||
|
||||
internal static void UInt32ToLe(uint n, byte[] bs, int off)
|
||||
{
|
||||
bs[off] = (byte)n;
|
||||
bs[off + 1] = (byte)(n >> 8);
|
||||
bs[off + 2] = (byte)(n >> 16);
|
||||
bs[off + 3] = (byte)(n >> 24);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -77,6 +77,16 @@ namespace CryptoExchange.Net.Clients
|
||||
{ new HttpMethod("Patch"), HttpMethodParameterPosition.InBody },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Encoding/charset for the ContentType header
|
||||
/// </summary>
|
||||
protected Encoding? RequestBodyContentEncoding { get; set; } = Encoding.UTF8;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to omit the ContentType header if there is no content
|
||||
/// </summary>
|
||||
protected bool OmitContentTypeHeaderWithoutContent { get; set; } = false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions;
|
||||
|
||||
@ -374,7 +384,11 @@ namespace CryptoExchange.Net.Clients
|
||||
if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?"))
|
||||
queryString = $"?{queryString}";
|
||||
|
||||
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
|
||||
var path = baseAddress.AppendPath(definition.Path);
|
||||
if (definition.ForcePathEndWithSlash == true && !path.EndsWith("/"))
|
||||
path += "/";
|
||||
|
||||
var uri = new Uri(path + queryString);
|
||||
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
|
||||
request.Accept = MessageHandler.AcceptHeader;
|
||||
|
||||
@ -398,14 +412,14 @@ namespace CryptoExchange.Net.Clients
|
||||
var bodyContent = requestConfiguration.GetBodyContent();
|
||||
if (bodyContent != null)
|
||||
{
|
||||
request.SetContent(bodyContent, contentType);
|
||||
request.SetContent(bodyContent, RequestBodyContentEncoding, contentType);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0)
|
||||
WriteParamBody(request, requestConfiguration.BodyParameters, contentType);
|
||||
else
|
||||
request.SetContent(RequestBodyEmptyContent, contentType);
|
||||
else if (OmitContentTypeHeaderWithoutContent != true)
|
||||
request.SetContent(RequestBodyEmptyContent, RequestBodyContentEncoding, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
@ -646,13 +660,13 @@ namespace CryptoExchange.Net.Clients
|
||||
stringData = stringSerializer.Serialize(value);
|
||||
else
|
||||
stringData = stringSerializer.Serialize(parameters);
|
||||
request.SetContent(stringData, contentType);
|
||||
request.SetContent(stringData, RequestBodyContentEncoding, contentType);
|
||||
}
|
||||
else if (contentType == Constants.FormContentHeader)
|
||||
{
|
||||
// Write the parameters as form data in the body
|
||||
var stringData = parameters.ToFormData();
|
||||
request.SetContent(stringData, contentType);
|
||||
request.SetContent(stringData, RequestBodyContentEncoding, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,9 +6,9 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||
<PackageVersion>10.7.2</PackageVersion>
|
||||
<AssemblyVersion>10.7.2</AssemblyVersion>
|
||||
<FileVersion>10.7.2</FileVersion>
|
||||
<PackageVersion>10.8.0</PackageVersion>
|
||||
<AssemblyVersion>10.8.0</AssemblyVersion>
|
||||
<FileVersion>10.8.0</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -43,9 +44,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Set string content
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="contentType"></param>
|
||||
void SetContent(string data, string contentType);
|
||||
void SetContent(string data, Encoding? encoding, string contentType);
|
||||
|
||||
/// <summary>
|
||||
/// Add a header to the request
|
||||
|
||||
@ -72,6 +72,11 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public int? ConnectionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the endpoint path should always include the trailing `/`
|
||||
/// </summary>
|
||||
public bool? ForcePathEndWithSlash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
|
||||
@ -47,6 +47,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="arraySerialization">Array serialization type</param>
|
||||
/// <param name="preventCaching">Prevent request caching</param>
|
||||
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
|
||||
/// <param name="forcePathEndWithSlash">Force trailing `/`</param>
|
||||
/// <returns></returns>
|
||||
public RequestDefinition GetOrCreate(
|
||||
HttpMethod method,
|
||||
@ -59,8 +60,9 @@ namespace CryptoExchange.Net.Objects
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
bool? preventCaching = null,
|
||||
bool? tryParseOnNonSuccess = null)
|
||||
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess);
|
||||
bool? tryParseOnNonSuccess = null,
|
||||
bool? forcePathEndWithSlash = null)
|
||||
=> GetOrCreate(method + path, method, path, rateLimitGate, weight, authenticated, limitGuard, requestBodyFormat, parameterPosition, arraySerialization, preventCaching, tryParseOnNonSuccess, forcePathEndWithSlash);
|
||||
|
||||
/// <summary>
|
||||
/// Get a definition if it is already in the cache or create a new definition and add it to the cache
|
||||
@ -77,6 +79,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <param name="arraySerialization">Array serialization type</param>
|
||||
/// <param name="preventCaching">Prevent request caching</param>
|
||||
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
|
||||
/// <param name="forcePathEndWithSlash">Force trailing `/`</param>
|
||||
/// <returns></returns>
|
||||
public RequestDefinition GetOrCreate(
|
||||
string identifier,
|
||||
@ -90,7 +93,8 @@ namespace CryptoExchange.Net.Objects
|
||||
HttpMethodParameterPosition? parameterPosition = null,
|
||||
ArrayParametersSerialization? arraySerialization = null,
|
||||
bool? preventCaching = null,
|
||||
bool? tryParseOnNonSuccess = null)
|
||||
bool? tryParseOnNonSuccess = null,
|
||||
bool? forcePathEndWithSlash = null)
|
||||
{
|
||||
|
||||
if (!_definitions.TryGetValue(identifier, out var def))
|
||||
@ -105,7 +109,8 @@ namespace CryptoExchange.Net.Objects
|
||||
RequestBodyFormat = requestBodyFormat,
|
||||
ParameterPosition = parameterPosition,
|
||||
PreventCaching = preventCaching ?? false,
|
||||
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false
|
||||
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false,
|
||||
ForcePathEndWithSlash = forcePathEndWithSlash ?? false
|
||||
};
|
||||
_definitions.TryAdd(identifier, def);
|
||||
}
|
||||
|
||||
@ -19,9 +19,6 @@ namespace CryptoExchange.Net.Requests
|
||||
/// <summary>
|
||||
/// Create request object for web request
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="requestId"></param>
|
||||
public Request(HttpRequestMessage request, HttpClient client, int requestId)
|
||||
{
|
||||
_httpClient = client;
|
||||
@ -55,10 +52,12 @@ namespace CryptoExchange.Net.Requests
|
||||
public int RequestId { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetContent(string data, string contentType)
|
||||
public void SetContent(string data, Encoding? encoding, string contentType)
|
||||
{
|
||||
Content = data;
|
||||
_request.Content = new StringContent(data, Encoding.UTF8, contentType);
|
||||
_request.Content = new StringContent(data, encoding ?? Encoding.UTF8, contentType);
|
||||
if (encoding == null)
|
||||
_request.Content.Headers.ContentType!.CharSet = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -46,7 +46,7 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
Content = Encoding.UTF8.GetString(data);
|
||||
}
|
||||
|
||||
public void SetContent(string data, string contentType)
|
||||
public void SetContent(string data, Encoding? encoding, string contentType)
|
||||
{
|
||||
Content = data;
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|
||||
||Bitget|CEX|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[](https://www.nuget.org/packages/JK.Bitget.Net)|[Link](https://partner.bitget.com/bg/1qlf6pj1)|20%|
|
||||
||BitMart|CEX|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[](https://www.nuget.org/packages/BitMart.Net)|[Link](https://www.bitmart.com/invite/JKorfAPI/en-US)|30%|
|
||||
||BitMEX|CEX|[JKorf/BitMEX.Net](https://github.com/JKorf/BitMEX.Net)|[](https://www.nuget.org/packages/JKorf.BitMEX.Net)|[Link](https://www.bitmex.com/app/register/94f98e)|30%|
|
||||
||Bitstamp|CEX|[JKorf/Bitstamp.Net](https://github.com/JKorf/Bitstamp.Net)|[](https://www.nuget.org/packages/Bitstamp.Net)|-|-|
|
||||
||BloFin|CEX|[JKorf/BloFin.Net](https://github.com/JKorf/BloFin.Net)|[](https://www.nuget.org/packages/BloFin.Net)|-|-|
|
||||
||Bybit|CEX|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[](https://www.nuget.org/packages/Bybit.Net)|[Link](https://partner.bybit.com/b/jkorf)|-|
|
||||
||Coinbase|CEX|[JKorf/Coinbase.Net](https://github.com/JKorf/Coinbase.Net)|[](https://www.nuget.org/packages/JKorf.Coinbase.Net)|[Link](https://advanced.coinbase.com/join/T6H54H8)|-|
|
||||
@ -67,6 +68,11 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
|
||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||
|
||||
## Release notes
|
||||
* Version 10.8.0 - 06 Mar 2026
|
||||
* Added `RequestBodyContentEncoding` and `OmitContentTypeHeaderWithoutContent` config to RestApiClient
|
||||
* Added `ForcePathEndWithSlash` setting to RequestDefinition
|
||||
* Added `encoding` parameter to SetContent on REST Request
|
||||
|
||||
* Version 10.7.2 - 02 Mar 2026
|
||||
* Added small overlap in UserDataTracker polling logic to account for API endpoints not immediately having the data available
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user