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; } } }