Skip to content

Commit fd68d52

Browse files
authored
ENS Utils (#63)
1 parent cac4184 commit fd68d52

File tree

3 files changed

+225
-2
lines changed

3 files changed

+225
-2
lines changed

Thirdweb.Tests/Thirdweb.Utils/Thirdweb.Utils.Tests.cs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ public void HashPrefixedMessage()
3535
public void HashMessage()
3636
{
3737
var messageStr = "Hello, World!";
38+
var hashStr = "0x" + Utils.HashMessage(messageStr);
39+
3840
var message = System.Text.Encoding.UTF8.GetBytes(messageStr);
39-
var hashStr = Utils.HashMessage(messageStr);
4041
var hash = Utils.HashMessage(message);
42+
4143
Assert.Equal(hashStr, Utils.BytesToHex(hash));
4244
Assert.Equal("0xacaf3289d7b601cbd114fb36c4d29c85bbfd5e133f14cb355c3fd8d99367964f", hashStr);
4345
}
@@ -515,4 +517,71 @@ public void HexToBytes32_ReturnsCorrectly()
515517
var hexTooLarge = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
516518
_ = Assert.Throws<ArgumentException>(() => Utils.HexToBytes32(hexTooLarge));
517519
}
520+
521+
[Fact(Timeout = 120000)]
522+
public async Task GetENSFromAddress_ThrowsException_WhenAddressIsNull()
523+
{
524+
var exception = await Assert.ThrowsAsync<ArgumentNullException>(async () => await Utils.GetENSFromAddress(this.Client, null));
525+
526+
Assert.Equal("address", exception.ParamName);
527+
}
528+
529+
[Fact(Timeout = 120000)]
530+
public async Task GetENSFromAddress_ThrowsException_WhenAddressIsInvalid()
531+
{
532+
var invalidAddress = "invalid_address";
533+
var exception = await Assert.ThrowsAsync<ArgumentException>(async () => await Utils.GetENSFromAddress(this.Client, invalidAddress));
534+
535+
Assert.Contains("Invalid address", exception.Message);
536+
}
537+
538+
[Fact(Timeout = 120000)]
539+
public async Task GetENSFromAddress_ReturnsENSName_WhenAddressIsValid()
540+
{
541+
var validAddress = "0xDaaBDaaC8073A7dAbdC96F6909E8476ab4001B34";
542+
var expectedENSName = "0xfirekeeper.eth";
543+
var ensName = await Utils.GetENSFromAddress(this.Client, validAddress);
544+
545+
Assert.Equal(expectedENSName, ensName);
546+
547+
ensName = await Utils.GetENSFromAddress(this.Client, validAddress);
548+
Assert.Equal(expectedENSName, ensName);
549+
}
550+
551+
[Fact(Timeout = 120000)]
552+
public async Task GetAddressFromENS_ThrowsException_WhenENSNameIsNull()
553+
{
554+
var exception = await Assert.ThrowsAsync<ArgumentNullException>(async () => await Utils.GetAddressFromENS(this.Client, null));
555+
Assert.Equal("ensName", exception.ParamName);
556+
}
557+
558+
[Fact(Timeout = 120000)]
559+
public async Task GetAddressFromENS_ReturnsENSName_WhenENSNameIsAlreadyAddress()
560+
{
561+
var address = "0xDaaBDaaC8073A7dAbdC96F6909E8476ab4001B34".ToLower();
562+
var result = await Utils.GetAddressFromENS(this.Client, address);
563+
Assert.Equal(address.ToChecksumAddress(), result);
564+
}
565+
566+
[Fact(Timeout = 120000)]
567+
public async Task GetAddressFromENS_ThrowsException_WhenENSNameIsInvalid()
568+
{
569+
var invalidENSName = "invalid_name";
570+
var exception = await Assert.ThrowsAsync<ArgumentException>(async () => await Utils.GetAddressFromENS(this.Client, invalidENSName));
571+
572+
Assert.Contains("Invalid ENS name", exception.Message);
573+
}
574+
575+
[Fact(Timeout = 120000)]
576+
public async Task GetAddressFromENS_ReturnsAddress_WhenENSNameIsValid()
577+
{
578+
var validENSName = "0xfirekeeper.eth";
579+
var expectedAddress = "0xDaaBDaaC8073A7dAbdC96F6909E8476ab4001B34";
580+
var result = await Utils.GetAddressFromENS(this.Client, validENSName);
581+
582+
Assert.Equal(expectedAddress.ToChecksumAddress(), result);
583+
584+
result = await Utils.GetAddressFromENS(this.Client, validENSName);
585+
Assert.Equal(expectedAddress.ToChecksumAddress(), result);
586+
}
518587
}

Thirdweb/Thirdweb.Utils/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public static class Constants
1919
"0x0101010101010101010101010101010101010101000000000000000000000000000000000000000000000000000001010101010100000000000000000000000000000000000000000000000000000000000000000101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101";
2020
internal const string FALLBACK_IPFS_GATEWAY = "https://ipfs.io/ipfs/";
2121
internal const string PIN_URI = "https://storage.thirdweb.com/ipfs/upload";
22+
internal const string ENS_REGISTRY_ADDRESS = "0xce01f8eee7E479C928F8919abD53E553a36CeF67";
2223

2324
internal const string ENTRYPOINT_V06_ABI =
2425
/*lang=json,strict*/

Thirdweb/Thirdweb.Utils/Utils.cs

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
using System.Security.Cryptography;
44
using System.Text;
55
using System.Text.RegularExpressions;
6+
using ADRaffy.ENSNormalize;
67
using Nethereum.ABI.EIP712;
8+
using Nethereum.ABI.FunctionEncoding;
9+
using Nethereum.ABI.FunctionEncoding.Attributes;
10+
using Nethereum.ABI.Model;
711
using Nethereum.Contracts;
812
using Nethereum.Hex.HexConvertors.Extensions;
913
using Nethereum.Signer;
@@ -20,6 +24,7 @@ public static partial class Utils
2024
{
2125
private static readonly Dictionary<BigInteger, bool> _eip155EnforcedCache = new();
2226
private static readonly Dictionary<BigInteger, ThirdwebChainData> _chainDataCache = new();
27+
private static readonly Dictionary<string, string> _ensCache = new();
2328

2429
/// <summary>
2530
/// Computes the client ID from the given secret key.
@@ -101,7 +106,7 @@ public static byte[] HashMessage(this byte[] messageBytes)
101106
/// <returns>The hashed message.</returns>
102107
public static string HashMessage(this string message)
103108
{
104-
return Sha3Keccack.Current.CalculateHash(Encoding.UTF8.GetBytes(message)).ToHex(true);
109+
return Sha3Keccack.Current.CalculateHash(message);
105110
}
106111

107112
/// <summary>
@@ -682,4 +687,152 @@ public static bool IsEip1559Supported(string chainId)
682687
return true;
683688
}
684689
}
690+
691+
#if NET8_0_OR_GREATER
692+
[GeneratedRegex("^\\.|\\.$")]
693+
private static partial Regex PacketRegex();
694+
#endif
695+
696+
public static byte[] PacketToBytes(string packet)
697+
{
698+
#if !NET8_0_OR_GREATER
699+
var value = new Regex("^\\.|\\.$").Replace(packet, "");
700+
#else
701+
var value = PacketRegex().Replace(packet, "");
702+
#endif
703+
if (string.IsNullOrEmpty(value))
704+
{
705+
return new byte[] { 0 };
706+
}
707+
708+
var labels = value.Split(".");
709+
using var memoryStream = new MemoryStream();
710+
foreach (var label in labels)
711+
{
712+
byte[] labelBytes;
713+
714+
if (label.Length > 63)
715+
{
716+
labelBytes = label.HashMessage().HexToBytes();
717+
}
718+
else
719+
{
720+
labelBytes = Encoding.UTF8.GetBytes(label);
721+
}
722+
723+
if (labelBytes.Length > 255)
724+
{
725+
throw new ArgumentException("Label is too long after encoding.");
726+
}
727+
728+
memoryStream.WriteByte((byte)labelBytes.Length);
729+
memoryStream.Write(labelBytes, 0, labelBytes.Length);
730+
}
731+
732+
memoryStream.WriteByte(0);
733+
return memoryStream.ToArray();
734+
}
735+
736+
public static async Task<string> GetENSFromAddress(ThirdwebClient client, string address)
737+
{
738+
if (string.IsNullOrEmpty(address))
739+
{
740+
throw new ArgumentNullException(nameof(address));
741+
}
742+
743+
if (!address.IsValidAddress())
744+
{
745+
throw new ArgumentException("Invalid address.");
746+
}
747+
748+
if (_ensCache.TryGetValue(address, out var value))
749+
{
750+
return value;
751+
}
752+
753+
var contract = await ThirdwebContract.Create(client: client, address: Constants.ENS_REGISTRY_ADDRESS, chain: 1).ConfigureAwait(false);
754+
var reverseName = address.ToLower()[2..] + ".addr.reverse";
755+
var reverseNameBytes = PacketToBytes(reverseName);
756+
var ensName = await contract.Read<string>("reverse", reverseNameBytes).ConfigureAwait(false);
757+
_ensCache[address] = ensName;
758+
return ensName;
759+
}
760+
761+
public static async Task<string> GetAddressFromENS(ThirdwebClient client, string ensName)
762+
{
763+
if (string.IsNullOrEmpty(ensName))
764+
{
765+
throw new ArgumentNullException(nameof(ensName));
766+
}
767+
768+
if (ensName.IsValidAddress())
769+
{
770+
return ensName.ToChecksumAddress();
771+
}
772+
773+
if (!ensName.Contains('.'))
774+
{
775+
throw new ArgumentException("Invalid ENS name.");
776+
}
777+
778+
if (_ensCache.TryGetValue(ensName, out var value))
779+
{
780+
return value;
781+
}
782+
783+
var registry = await ThirdwebContract.Create(client: client, address: Constants.ENS_REGISTRY_ADDRESS, chain: 1).ConfigureAwait(false);
784+
var functionCallEncoder = new FunctionCallEncoder();
785+
var encodedAddr = "addr(bytes32)"
786+
.HashMessage()
787+
.HexToBytes()
788+
.Take(4)
789+
.ToArray()
790+
.Concat(functionCallEncoder.EncodeParameters(new Parameter[] { new("bytes32", "name") }, new object[] { NameHash(ensName).HexToBytes() }))
791+
.ToArray();
792+
var result = await registry.Read<ResolveReturnType>("resolve(bytes name, bytes data)", PacketToBytes(ensName), encodedAddr).ConfigureAwait(false);
793+
var address = ("0x" + result.Bytes.BytesToHex()[26..]).ToChecksumAddress();
794+
_ensCache[ensName] = address;
795+
return address;
796+
}
797+
798+
public static bool IsValidAddress(this string address)
799+
{
800+
if (string.IsNullOrEmpty(address))
801+
{
802+
return false;
803+
}
804+
805+
if (address.StartsWith("0x") && address.Length == 42 && !address.Contains('.'))
806+
{
807+
return true;
808+
}
809+
810+
return false;
811+
}
812+
813+
[FunctionOutput]
814+
private class ResolveReturnType
815+
{
816+
[Parameter("bytes", "", 1)]
817+
public byte[] Bytes { get; set; }
818+
819+
[Parameter("address", "", 2)]
820+
public string Address { get; set; }
821+
}
822+
823+
private static string NameHash(string name)
824+
{
825+
var node = "0x0000000000000000000000000000000000000000000000000000000000000000";
826+
if (!string.IsNullOrEmpty(name))
827+
{
828+
name = ENSNormalize.ENSIP15.Normalize(name);
829+
var labels = name.Split('.');
830+
for (var i = labels.Length - 1; i >= 0; i--)
831+
{
832+
var byteInput = (node + labels[i].HashMessage()).HexToByteArray();
833+
node = byteInput.HashMessage().BytesToHex();
834+
}
835+
}
836+
return node.EnsureHexPrefix();
837+
}
685838
}

0 commit comments

Comments
 (0)