diff --git a/.gitignore b/.gitignore
index 86a11abc..478d4c35 100644
--- a/.gitignore
+++ b/.gitignore
@@ -287,3 +287,4 @@ __pycache__/
*.odx.cs
*.xsd.cs
CryptoExchange.Net/CryptoExchange.Net.xml
+CryptoExchange.Net.Web3/CryptoExchange.Net.Web3.xml
diff --git a/CryptoExchange.Net.Web3/CeAbiEncoder.cs b/CryptoExchange.Net.Web3/CeAbiEncoder.cs
new file mode 100644
index 00000000..e3aa0362
--- /dev/null
+++ b/CryptoExchange.Net.Web3/CeAbiEncoder.cs
@@ -0,0 +1,149 @@
+using System.Numerics;
+using System.Text;
+
+namespace CryptoExchange.Net.Web3
+{
+ ///
+ /// ABI encoding
+ ///
+ public static class CeAbiEncoder
+ {
+ ///
+ /// ABI encode string as Sha3Keccack hashed byte value
+ ///
+ public static byte[] AbiValueEncodeString(string value)
+ {
+ var abiValueEncoded = CeSha3Keccack.CalculateHash(Encoding.UTF8.GetBytes(value));
+ return abiValueEncoded;
+ }
+
+ ///
+ /// ABI encode bool value as uint256 with 1 for true and 0 for false, as per ABI specification
+ ///
+ public static byte[] AbiValueEncodeBool(bool value)
+ => AbiValueEncodeInt((byte)(value ? 1 : 0));
+
+ ///
+ /// ABI encode byte value as uint256, as per ABI specification
+ ///
+ public static byte[] AbiValueEncodeInt(byte value)
+ => AbiValueEncodeBigInteger(false, new BigInteger(value));
+
+ ///
+ /// ABI encode short value as int256, as per ABI specification
+ ///
+ public static byte[] AbiValueEncodeInt(short value)
+ => AbiValueEncodeBigInteger(true, new BigInteger(value));
+
+ ///
+ /// ABI encode int value as int256, as per ABI specification
+ ///
+ public static byte[] AbiValueEncodeInt(int value)
+ => AbiValueEncodeBigInteger(true, new BigInteger(value));
+
+ ///
+ /// ABI encode long value as int256, as per ABI specification
+ ///
+ public static byte[] AbiValueEncodeInt(long value)
+ => AbiValueEncodeBigInteger(true, new BigInteger(value));
+
+ ///
+ /// ABI encode ushort value as uint256, as per ABI specification
+ ///
+ public static byte[] AbiValueEncodeInt(ushort value)
+ => AbiValueEncodeBigInteger(false, new BigInteger(value));
+
+ ///
+ /// ABI encode uint value as uint256, as per ABI specification
+ ///
+ public static byte[] AbiValueEncodeInt(uint value)
+ => AbiValueEncodeBigInteger(false, new BigInteger(value));
+
+ ///
+ /// ABI encode ulong value as uint256, as per ABI specification
+ ///
+ public static byte[] AbiValueEncodeInt(ulong value)
+ => AbiValueEncodeBigInteger(false, new BigInteger(value));
+
+ ///
+ /// ABI encode big integer value as int256 or uint256, as per ABI specification
+ ///
+ 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;
+ }
+
+ ///
+ /// ABI encode address value as uint256, as per ABI specification
+ ///
+ public static byte[] AbiValueEncodeAddress(string value)
+ {
+ var result = new byte[32];
+ var h = value.HexStringToBytes();
+ h.CopyTo(result, result.Length - h.Length);
+ return result;
+ }
+
+ ///
+ /// 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.
+ ///
+ public static byte[] AbiValueEncodeHexBytes(int length, string value)
+ => AbiValueEncodeBytes(length, value.HexStringToBytes());
+
+ ///
+ /// ABI encode byte array value as bytes32, as per ABI specification.
+ /// The resulting bytes will be left aligned in the 32 bytes result, with trailing zeros if the byte array is shorter than 32 bytes.
+ /// If the byte array is longer than 32 bytes, an exception will be thrown.
+ ///
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CryptoExchange.Net.Web3/CeEip712TypedDataEncoder.cs b/CryptoExchange.Net.Web3/CeEip712TypedDataEncoder.cs
new file mode 100644
index 00000000..21f129f2
--- /dev/null
+++ b/CryptoExchange.Net.Web3/CeEip712TypedDataEncoder.cs
@@ -0,0 +1,342 @@
+using System.Collections;
+using System.Globalization;
+using System.Numerics;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace CryptoExchange.Net.Web3
+{
+ ///
+ /// EIP712 Typed Data Encoder
+ ///
+ public static class CeEip712TypedDataEncoder
+ {
+ ///
+ /// 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
+ ///
+ 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
+ {
+ {
+ "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);
+ }
+
+ ///
+ /// 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
+ ///
+ 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 types, string primaryType, IEnumerable 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 types, IEnumerable memberValues)
+ {
+ foreach (var memberValue in memberValues)
+ {
+ switch (memberValue.TypeName)
+ {
+ case var refType when IsReferenceType(refType):
+ writer.Write(HashStruct(types, memberValue.TypeName, (IEnumerable)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();
+ 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 types, string typeName)
+ {
+ var encodedTypes = EncodeTypes(types, typeName);
+ var encodedPrimaryType = encodedTypes.SingleOrDefault(x => x.Key == typeName);
+ if (encodedPrimaryType.Equals(default(KeyValuePair)))
+ 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> EncodeTypes(
+ IDictionary types,
+ string currentTypeName)
+ {
+ var visited = new HashSet(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 Encode(string typeName)
+ {
+ var members = types[typeName]
+ .Select(x => $"{x.Type} {x.Name}");
+
+ return new KeyValuePair(
+ typeName,
+ $"{typeName}({string.Join(",", members)})");
+ }
+
+ Visit(currentTypeName);
+
+ var result = new List>
+ {
+ 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;
+ }
+ }
+
+ ///
+ /// Member description
+ ///
+ public class CeMemberDescription
+ {
+ ///
+ /// Name
+ ///
+ public string Name { get; set; } = string.Empty;
+ ///
+ /// Type
+ ///
+ public string Type { get; set; } = string.Empty;
+ }
+
+ ///
+ /// Member value
+ ///
+ public class CeMemberValue
+ {
+ ///
+ /// Type name
+ ///
+ public string TypeName { get; set; } = string.Empty;
+ ///
+ /// Value
+ ///
+ public object Value { get; set; } = string.Empty;
+ }
+
+ ///
+ /// Typed data raw, used for encoding EIP712 typed data with the provided primary type, domain fields and message fields.
+ ///
+ public class CeTypedDataRaw
+ {
+ ///
+ /// Type dictionary
+ ///
+ public IDictionary Types { get; set; } = new Dictionary();
+ ///
+ /// Primary type
+ ///
+ public string PrimaryType { get; set; } = string.Empty;
+ ///
+ /// Message values
+ ///
+ public CeMemberValue[] Message { get; set; } = [];
+ ///
+ /// Domain values
+ ///
+ public CeMemberValue[] DomainRawValues { get; set; } = [];
+ }
+}
\ No newline at end of file
diff --git a/CryptoExchange.Net.Web3/CeSha3Keccack.cs b/CryptoExchange.Net.Web3/CeSha3Keccack.cs
new file mode 100644
index 00000000..ef5547c0
--- /dev/null
+++ b/CryptoExchange.Net.Web3/CeSha3Keccack.cs
@@ -0,0 +1,382 @@
+using System.Diagnostics;
+
+namespace CryptoExchange.Net.Web3
+{
+ ///
+ /// Sha3 Keccack hashing, as per Ethereum specification, with 256 bit output
+ ///
+ public class CeSha3Keccack
+ {
+ ///
+ /// Calculate the Keccack256 hash of the provided data, as per Ethereum specification
+ ///
+ 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);
+ }
+ }
+ }
+}
diff --git a/CryptoExchange.Net.Web3/CryptoExchange.Net.Web3.csproj b/CryptoExchange.Net.Web3/CryptoExchange.Net.Web3.csproj
new file mode 100644
index 00000000..18d1567f
--- /dev/null
+++ b/CryptoExchange.Net.Web3/CryptoExchange.Net.Web3.csproj
@@ -0,0 +1,51 @@
+
+
+ netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0
+
+
+ CryptoExchange.Net.Web3
+ JKorf
+ CryptoExchange.Net.Web3 is a helper library adding Web3 utilities to CryptoExchange.Net implementations.
+ 13.0.0
+ 13.0.0
+ 13.0.0
+ false
+ git
+ https://github.com/JKorf/CryptoExchange.Net.git
+ https://github.com/JKorf/CryptoExchange.Net
+ en
+ README.md
+ icon.png
+ true
+ https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes
+ enable
+ latest
+ MIT
+ enable
+
+
+
+
+
+
+ true
+
+
+ true
+ true
+ snupkg
+ true
+ true
+
+
+ CryptoExchange.Net.Web3.xml
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
diff --git a/CryptoExchange.Net.Web3/EthKey.cs b/CryptoExchange.Net.Web3/EthKey.cs
new file mode 100644
index 00000000..09002cf4
--- /dev/null
+++ b/CryptoExchange.Net.Web3/EthKey.cs
@@ -0,0 +1,171 @@
+using Secp256k1Net;
+using System.Text.Json.Serialization;
+
+namespace CryptoExchange.Net.Web3
+{
+ ///
+ /// Eth public/private key pair, used for signing messages and verifying signatures.
+ ///
+ public class EthKey
+ {
+ private byte[]? _pkBytes;
+
+ ///
+ /// Private key
+ ///
+ [JsonIgnore]
+ public string? PrivateKey { get; }
+ ///
+ /// Public address
+ ///
+ public string PublicAddress { get; }
+
+ ///
+ /// Create a new EthKey instance
+ ///
+ /// Public address, can not sign messages without private key
+ /// Private key, required for signing messages
+ ///
+ 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!);
+ }
+ }
+
+ ///
+ /// Create new Eth credentials with only a public key. This is generally only sufficient for verifying signatures and read operations.
+ /// Request signing will not be possible without a private key.
+ ///
+ /// The public wallet address
+ public static EthKey FromPublicAddress(string publicAddress)
+ {
+ return new EthKey(publicAddress, null);
+ }
+
+ ///
+ /// Create new Eth credentials from a private key. The public key will be derived from the private key. Allows signing of messages.
+ ///
+ /// The private key for the wallet
+ public static EthKey FromPrivateKey(string privateKey)
+ {
+ return new EthKey(null, privateKey);
+ }
+
+ ///
+ /// Sign a hash with the private key, returning the signature in rsv format
+ ///
+ public Dictionary 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(signature, 0, 32));
+ var hexCompactS = BytesToHexString(new ArraySegment(signature, 32, 32));
+ var hexCompactV = recover + 27;
+
+ return new Dictionary
+ {
+ { "r", "0x" + hexCompactR },
+ { "s", "0x" + hexCompactS },
+ { "v", hexCompactV },
+ };
+ }
+
+ ///
+ /// Sign a hash with the private key, returning the signature in signature format
+ ///
+ 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(signature, 0, 32));
+ var hexCompactS = BytesToHexString(new ArraySegment(signature, 32, 32));
+ var hexCompactV = BytesToHexString([(byte)(recover + 27)]);
+
+ var result = "0x" + hexCompactR.PadLeft(64, '0') +
+ hexCompactS.PadLeft(64, '0') +
+ hexCompactV;
+ return result;
+ }
+
+ ///
+ 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 buff)
+ {
+ string text = string.Empty;
+ foreach (byte item in buff)
+ text += item.ToString("X2");
+ return text;
+ }
+ }
+}
diff --git a/CryptoExchange.Net.Web3/MessagePackConverter.cs b/CryptoExchange.Net.Web3/MessagePackConverter.cs
new file mode 100644
index 00000000..08ae8751
--- /dev/null
+++ b/CryptoExchange.Net.Web3/MessagePackConverter.cs
@@ -0,0 +1,419 @@
+using System.Collections;
+using System.Globalization;
+using System.Text;
+
+namespace CryptoExchange.Net.Web3
+{
+ ///
+ /// Message pack converter, not thread safe
+ ///
+ public class MessagePackConverter
+ {
+ private byte[] _tmp0 = new byte[8];
+ private Encoding _encoder = Encoding.UTF8;
+
+ ///
+ /// Convert an object to byte array
+ ///
+ public byte[] Pack(object o)
+ {
+ using var ms = new MemoryStream();
+ Pack(ms, o);
+ return ms.ToArray();
+ }
+
+ ///
+ /// Convert an object to byte array
+ ///
+ 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 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 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);
+ }
+ }
+ }
+}
diff --git a/CryptoExchange.Net.sln b/CryptoExchange.Net.sln
index 39abdc12..5de3b3d6 100644
--- a/CryptoExchange.Net.sln
+++ b/CryptoExchange.Net.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.32014.148
+# Visual Studio Version 18
+VisualStudioVersion = 18.10.12201.205 stable
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoExchange.Net", "CryptoExchange.Net\CryptoExchange.Net.csproj", "{3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}"
EndProject
@@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\C
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptoExchange.Net.Web3", "CryptoExchange.Net.Web3\CryptoExchange.Net.Web3.csproj", "{7ABCD210-C708-42ED-AF81-85C0A6089A1E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -41,6 +43,10 @@ Global
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7ABCD210-C708-42ED-AF81-85C0A6089A1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7ABCD210-C708-42ED-AF81-85C0A6089A1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7ABCD210-C708-42ED-AF81-85C0A6089A1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7ABCD210-C708-42ED-AF81-85C0A6089A1E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE