diff --git a/Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.Types.cs b/Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.Types.cs
index e0a34cb1..0c2e6e61 100644
--- a/Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.Types.cs
+++ b/Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.Types.cs
@@ -76,6 +76,132 @@ public class ContractMetadata
///
[JsonProperty("image")]
public string Image { get; set; }
+
+ ///
+ /// Gets or sets the merkle tree mappings for claim conditions.
+ /// Key: Merkle root hash, Value: IPFS URI to tree info
+ ///
+ [JsonProperty("merkle")]
+ public Dictionary Merkle { get; set; }
+}
+
+#endregion
+
+#region Merkle
+
+///
+/// Represents information about a sharded Merkle tree stored on IPFS.
+///
+public class MerkleTreeInfo
+{
+ ///
+ /// Gets or sets the Merkle root hash.
+ ///
+ [JsonProperty("merkleRoot")]
+ public string MerkleRoot { get; set; }
+
+ ///
+ /// Gets or sets the base IPFS URI for shard files.
+ ///
+ [JsonProperty("baseUri")]
+ public string BaseUri { get; set; }
+
+ ///
+ /// Gets or sets the original entries IPFS URI.
+ ///
+ [JsonProperty("originalEntriesUri")]
+ public string OriginalEntriesUri { get; set; }
+
+ ///
+ /// Gets or sets the number of hex characters used for shard keys.
+ ///
+ [JsonProperty("shardNybbles")]
+ public int ShardNybbles { get; set; } = 2;
+
+ ///
+ /// Gets or sets the token decimals for price calculations.
+ ///
+ [JsonProperty("tokenDecimals")]
+ public int TokenDecimals { get; set; } = 0;
+}
+
+///
+/// Represents a shard file containing whitelist entries and proofs.
+///
+public class ShardData
+{
+ ///
+ /// Gets or sets the shard proofs (path from shard root to main root).
+ ///
+ [JsonProperty("proofs")]
+ public List Proofs { get; set; }
+
+ ///
+ /// Gets or sets the whitelist entries in this shard.
+ ///
+ [JsonProperty("entries")]
+ public List Entries { get; set; }
+}
+
+///
+/// Represents a whitelist entry for a claim condition.
+///
+public class WhitelistEntry
+{
+ ///
+ /// Gets or sets the wallet address.
+ ///
+ [JsonProperty("address")]
+ public string Address { get; set; }
+
+ ///
+ /// Gets or sets the maximum claimable amount ("unlimited" or a number).
+ ///
+ [JsonProperty("maxClaimable")]
+ public string MaxClaimable { get; set; }
+
+ ///
+ /// Gets or sets the price override ("unlimited" or a number in wei).
+ ///
+ [JsonProperty("price")]
+ public string Price { get; set; }
+
+ ///
+ /// Gets or sets the currency address for price override.
+ ///
+ [JsonProperty("currencyAddress")]
+ public string CurrencyAddress { get; set; }
+}
+
+///
+/// Represents an allowlist proof for claiming tokens.
+///
+[FunctionOutput]
+public class AllowlistProof
+{
+ ///
+ /// Gets or sets the Merkle proof (array of bytes32 hashes).
+ ///
+ [Parameter("bytes32[]", "proof", 1)]
+ public List Proof { get; set; }
+
+ ///
+ /// Gets or sets the maximum quantity this address can claim.
+ ///
+ [Parameter("uint256", "quantityLimitPerWallet", 2)]
+ public BigInteger QuantityLimitPerWallet { get; set; }
+
+ ///
+ /// Gets or sets the price per token override.
+ ///
+ [Parameter("uint256", "pricePerToken", 3)]
+ public BigInteger PricePerToken { get; set; }
+
+ ///
+ /// Gets or sets the currency address for the price override.
+ ///
+ [Parameter("address", "currency", 4)]
+ public string Currency { get; set; }
}
#endregion
diff --git a/Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.cs b/Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.cs
index 869344c0..75fdc5a6 100644
--- a/Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.cs
+++ b/Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.cs
@@ -6,6 +6,8 @@ namespace Thirdweb;
public static class ThirdwebExtensions
{
+ private static readonly BigInteger MaxUint256 = BigInteger.Parse(Constants.MAX_UINT256_STR);
+
#region Common
///
@@ -1555,6 +1557,161 @@ public static async Task> ERC1155_GetOwnedNFTs(this ThirdwebContract c
#endregion
+ #region Merkle
+
+ ///
+ /// Gets the allowlist proof for a wallet address to claim from a drop contract.
+ /// This fetches the Merkle tree data from IPFS and calculates the proof locally.
+ ///
+ /// The drop contract to interact with.
+ /// The wallet address to get the proof for.
+ /// Optional claim condition ID. If not provided, uses the active condition.
+ /// Optional token ID for ERC1155 drops.
+ ///
+ /// An with an empty proof array for public mints (no allowlist restriction),
+ /// or null if the wallet is not in the allowlist or the merkle root is not found in contract metadata.
+ ///
+ /// Thrown when the contract is null.
+ /// Thrown when the wallet address is null or empty.
+ public static async Task GetAllowlistProof(
+ this ThirdwebContract contract,
+ string walletAddress,
+ BigInteger? claimConditionId = null,
+ BigInteger? tokenId = null)
+ {
+ if (contract == null)
+ {
+ throw new ArgumentNullException(nameof(contract));
+ }
+
+ if (string.IsNullOrEmpty(walletAddress))
+ {
+ throw new ArgumentException("Wallet address must be provided", nameof(walletAddress));
+ }
+
+ // Get contract metadata
+ var contractUri = await ThirdwebContract.Read(contract, "contractURI").ConfigureAwait(false);
+ var metadata = await ThirdwebStorage.Download(contract.Client, contractUri).ConfigureAwait(false);
+
+ if (metadata?.Merkle == null || metadata.Merkle.Count == 0)
+ {
+ // No merkle data, return empty proof (public mint)
+ return new AllowlistProof
+ {
+ Proof = new List(),
+ QuantityLimitPerWallet = BigInteger.Zero,
+ PricePerToken = BigInteger.Parse(Constants.MAX_UINT256_STR), // MAX_UINT256
+ Currency = Constants.ADDRESS_ZERO
+ };
+ }
+
+ // Get claim condition
+ Drop_ClaimCondition claimCondition;
+ if (claimConditionId.HasValue)
+ {
+ if (tokenId.HasValue)
+ {
+ claimCondition = await ThirdwebContract.Read(contract, "getClaimConditionById", tokenId.Value, claimConditionId.Value).ConfigureAwait(false);
+ }
+ else
+ {
+ claimCondition = await ThirdwebContract.Read(contract, "getClaimConditionById", claimConditionId.Value).ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ BigInteger activeId;
+ if (tokenId.HasValue)
+ {
+ activeId = await ThirdwebContract.Read(contract, "getActiveClaimConditionId", tokenId.Value).ConfigureAwait(false);
+ claimCondition = await ThirdwebContract.Read(contract, "getClaimConditionById", tokenId.Value, activeId).ConfigureAwait(false);
+ }
+ else
+ {
+ activeId = await ThirdwebContract.Read(contract, "getActiveClaimConditionId").ConfigureAwait(false);
+ claimCondition = await ThirdwebContract.Read(contract, "getClaimConditionById", activeId).ConfigureAwait(false);
+ }
+ }
+
+ // Check if it's a public mint (zero merkle root)
+ var merkleRootHex = claimCondition.MerkleRoot.BytesToHex();
+ if (merkleRootHex == "0x0000000000000000000000000000000000000000000000000000000000000000")
+ {
+ // Public mint, no proof needed
+ return new AllowlistProof
+ {
+ Proof = new List(),
+ QuantityLimitPerWallet = BigInteger.Zero,
+ PricePerToken = BigInteger.Parse(Constants.MAX_UINT256_STR),
+ Currency = Constants.ADDRESS_ZERO
+ };
+ }
+
+ // Find the tree info URI for this merkle root
+ if (!metadata.Merkle.TryGetValue(merkleRootHex, out var treeInfoUri))
+ {
+ // Try without 0x prefix or with different case
+ var found = false;
+ foreach (var kvp in metadata.Merkle)
+ {
+ if (kvp.Key.Equals(merkleRootHex, StringComparison.OrdinalIgnoreCase))
+ {
+ treeInfoUri = kvp.Value;
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ return null; // Merkle root not found in metadata
+ }
+ }
+
+ // Download tree info
+ var treeInfo = await ThirdwebStorage.Download(contract.Client, treeInfoUri).ConfigureAwait(false);
+ if (treeInfo?.BaseUri == null)
+ {
+ return null;
+ }
+
+ // Calculate shard key and download shard
+ var shardKey = MerkleTreeUtils.GetShardKey(walletAddress, treeInfo.ShardNybbles);
+ var shardUri = $"{treeInfo.BaseUri}/{shardKey}.json";
+
+ // Helper to check if exception indicates "not found" (expected when wallet not in allowlist)
+ static bool IsNotFoundError(Exception ex) =>
+ ex.Message.Contains("NotFound", StringComparison.OrdinalIgnoreCase) ||
+ ex.Message.Contains("404", StringComparison.Ordinal);
+
+ ShardData shardData;
+ try
+ {
+ shardData = await ThirdwebStorage.Download(contract.Client, shardUri).ConfigureAwait(false);
+ }
+ catch (Exception ex) when (IsNotFoundError(ex))
+ {
+ // Try without .json extension (some IPFS gateways don't need it)
+ try
+ {
+ shardUri = $"{treeInfo.BaseUri}/{shardKey}";
+ shardData = await ThirdwebStorage.Download(contract.Client, shardUri).ConfigureAwait(false);
+ }
+ catch (Exception ex2) when (IsNotFoundError(ex2))
+ {
+ // Shard not found - wallet is not in the allowlist (expected case)
+ return null;
+ }
+ // Other errors (network, auth, etc.) propagate up as unexpected
+ }
+ // Other errors from first attempt propagate up as unexpected
+
+ // Calculate proof
+ return MerkleTreeUtils.CalculateMerkleProof(shardData, walletAddress);
+ }
+
+ #endregion
+
#region DropERC20
///
@@ -1601,8 +1758,31 @@ public static async Task DropERC20_Claim(this Thirdw
var payableAmount = isNativeToken ? rawAmountToClaim * activeClaimCondition.PricePerToken / BigInteger.Pow(10, 18) : BigInteger.Zero;
- // TODO: Merkle
- var allowlistProof = new object[] { Array.Empty(), BigInteger.Zero, BigInteger.Zero, Constants.ADDRESS_ZERO };
+ // Get merkle proof for allowlist
+ var allowlistProofData = await contract.GetAllowlistProof(receiverAddress).ConfigureAwait(false);
+ object[] allowlistProof;
+ if (allowlistProofData != null && allowlistProofData.Proof != null && allowlistProofData.Proof.Count > 0)
+ {
+ allowlistProof = new object[]
+ {
+ allowlistProofData.Proof.ToArray(),
+ allowlistProofData.QuantityLimitPerWallet,
+ allowlistProofData.PricePerToken,
+ allowlistProofData.Currency
+ };
+
+ // Recalculate payable amount if allowlist has price override
+ if (allowlistProofData.PricePerToken < MaxUint256)
+ {
+ var allowlistCurrency = allowlistProofData.Currency;
+ isNativeToken = allowlistCurrency == Constants.NATIVE_TOKEN_ADDRESS || allowlistCurrency == Constants.ADDRESS_ZERO;
+ payableAmount = isNativeToken ? rawAmountToClaim * allowlistProofData.PricePerToken / BigInteger.Pow(10, 18) : BigInteger.Zero;
+ }
+ }
+ else
+ {
+ allowlistProof = new object[] { Array.Empty(), BigInteger.Zero, BigInteger.Zero, Constants.ADDRESS_ZERO };
+ }
var fnArgs = new object[]
{
@@ -1739,8 +1919,31 @@ public static async Task DropERC721_Claim(this Third
var payableAmount = isNativeToken ? quantity * activeClaimCondition.PricePerToken : BigInteger.Zero;
- // TODO: Merkle
- var allowlistProof = new object[] { Array.Empty(), BigInteger.Zero, BigInteger.Zero, Constants.ADDRESS_ZERO };
+ // Get merkle proof for allowlist
+ var allowlistProofData = await contract.GetAllowlistProof(receiverAddress).ConfigureAwait(false);
+ object[] allowlistProof;
+ if (allowlistProofData != null && allowlistProofData.Proof != null && allowlistProofData.Proof.Count > 0)
+ {
+ allowlistProof = new object[]
+ {
+ allowlistProofData.Proof.ToArray(),
+ allowlistProofData.QuantityLimitPerWallet,
+ allowlistProofData.PricePerToken,
+ allowlistProofData.Currency
+ };
+
+ // Recalculate payable amount if allowlist has price override
+ if (allowlistProofData.PricePerToken < MaxUint256)
+ {
+ var allowlistCurrency = allowlistProofData.Currency;
+ isNativeToken = allowlistCurrency == Constants.NATIVE_TOKEN_ADDRESS || allowlistCurrency == Constants.ADDRESS_ZERO;
+ payableAmount = isNativeToken ? quantity * allowlistProofData.PricePerToken : BigInteger.Zero;
+ }
+ }
+ else
+ {
+ allowlistProof = new object[] { Array.Empty(), BigInteger.Zero, BigInteger.Zero, Constants.ADDRESS_ZERO };
+ }
var fnArgs = new object[]
{
@@ -1902,8 +2105,31 @@ public static async Task DropERC1155_Claim(this Thir
var payableAmount = isNativeToken ? quantity * activeClaimCondition.PricePerToken : BigInteger.Zero;
- // TODO: Merkle
- var allowlistProof = new object[] { Array.Empty(), BigInteger.Zero, BigInteger.Zero, Constants.ADDRESS_ZERO };
+ // Get merkle proof for allowlist (passing tokenId for ERC1155)
+ var allowlistProofData = await contract.GetAllowlistProof(receiverAddress, null, tokenId).ConfigureAwait(false);
+ object[] allowlistProof;
+ if (allowlistProofData != null && allowlistProofData.Proof != null && allowlistProofData.Proof.Count > 0)
+ {
+ allowlistProof = new object[]
+ {
+ allowlistProofData.Proof.ToArray(),
+ allowlistProofData.QuantityLimitPerWallet,
+ allowlistProofData.PricePerToken,
+ allowlistProofData.Currency
+ };
+
+ // Recalculate payable amount if allowlist has price override
+ if (allowlistProofData.PricePerToken < MaxUint256)
+ {
+ var allowlistCurrency = allowlistProofData.Currency;
+ isNativeToken = allowlistCurrency == Constants.NATIVE_TOKEN_ADDRESS || allowlistCurrency == Constants.ADDRESS_ZERO;
+ payableAmount = isNativeToken ? quantity * allowlistProofData.PricePerToken : BigInteger.Zero;
+ }
+ }
+ else
+ {
+ allowlistProof = new object[] { Array.Empty(), BigInteger.Zero, BigInteger.Zero, Constants.ADDRESS_ZERO };
+ }
var fnArgs = new object[]
{
diff --git a/Thirdweb/Thirdweb.Utils/Constants.cs b/Thirdweb/Thirdweb.Utils/Constants.cs
index 08267613..9bee8ac6 100644
--- a/Thirdweb/Thirdweb.Utils/Constants.cs
+++ b/Thirdweb/Thirdweb.Utils/Constants.cs
@@ -13,6 +13,7 @@ public static class Constants
public const string ADDRESS_ZERO = "0x0000000000000000000000000000000000000000";
public const string NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
public const double DECIMALS_18 = 1000000000000000000;
+ public const string MAX_UINT256_STR = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
public const string IERC20_INTERFACE_ID = "0x36372b07";
public const string IERC721_INTERFACE_ID = "0x80ac58cd";
diff --git a/Thirdweb/Thirdweb.Utils/MerkleTree.cs b/Thirdweb/Thirdweb.Utils/MerkleTree.cs
new file mode 100644
index 00000000..b4f35588
--- /dev/null
+++ b/Thirdweb/Thirdweb.Utils/MerkleTree.cs
@@ -0,0 +1,238 @@
+using System.Numerics;
+using Nethereum.ABI;
+using Nethereum.Util;
+
+namespace Thirdweb;
+
+///
+/// Provides utilities for Merkle tree operations compatible with OpenZeppelin and Thirdweb standards.
+///
+public static class MerkleTreeUtils
+{
+ private static readonly BigInteger _maxUint256 = BigInteger.Parse("115792089237316195423570985008687907853269984665640564039457584007913129639935");
+
+ ///
+ /// Computes the Thirdweb-compatible leaf hash for a whitelist entry.
+ /// Format: keccak256(encodePacked(address, uint256 maxClaimable, uint256 price, address currency))
+ ///
+ /// The whitelist entry to hash.
+ /// The 32-byte keccak256 hash of the encoded entry.
+ public static byte[] HashLeaf(WhitelistEntry entry)
+ {
+ var address = entry.Address.ToLower();
+
+ // maxClaimable: "unlimited" or missing = MAX_UINT256
+ var maxClaimable = string.IsNullOrEmpty(entry.MaxClaimable) || string.Equals(entry.MaxClaimable, "unlimited", StringComparison.OrdinalIgnoreCase)
+ ? _maxUint256
+ : BigInteger.Parse(entry.MaxClaimable);
+
+ // price: "unlimited" or missing = MAX_UINT256
+ var price = string.IsNullOrEmpty(entry.Price) || string.Equals(entry.Price, "unlimited", StringComparison.OrdinalIgnoreCase)
+ ? _maxUint256
+ : BigInteger.Parse(entry.Price);
+
+ // currency: missing = zero address
+ var currency = string.IsNullOrEmpty(entry.CurrencyAddress)
+ ? Constants.ADDRESS_ZERO
+ : entry.CurrencyAddress.ToLower();
+
+ // ABI encode packed
+ var abiEncode = new ABIEncode();
+ var packed = abiEncode.GetABIEncodedPacked(
+ new ABIValue("address", address),
+ new ABIValue("uint256", maxClaimable),
+ new ABIValue("uint256", price),
+ new ABIValue("address", currency)
+ );
+
+ // keccak256
+ return Sha3Keccack.Current.CalculateHash(packed);
+ }
+
+ ///
+ /// Compares two byte arrays lexicographically (for OpenZeppelin-compatible sorting).
+ ///
+ private static int CompareBytes(byte[] a, byte[] b)
+ {
+ for (var i = 0; i < Math.Min(a.Length, b.Length); i++)
+ {
+ if (a[i] < b[i])
+ {
+ return -1;
+ }
+
+ if (a[i] > b[i])
+ {
+ return 1;
+ }
+ }
+
+ return a.Length.CompareTo(b.Length);
+ }
+
+ ///
+ /// Hashes a pair of nodes in sorted order (OpenZeppelin standard).
+ ///
+ private static byte[] HashPair(byte[] a, byte[] b)
+ {
+ // Sort: smaller first
+ var (first, second) = CompareBytes(a, b) < 0 ? (a, b) : (b, a);
+
+ // Concatenate and hash
+ var combined = new byte[first.Length + second.Length];
+ Buffer.BlockCopy(first, 0, combined, 0, first.Length);
+ Buffer.BlockCopy(second, 0, combined, first.Length, second.Length);
+
+ return Sha3Keccack.Current.CalculateHash(combined);
+ }
+
+ ///
+ /// Calculates the full Merkle proof for a claimer address from shard data.
+ ///
+ /// The shard data containing entries and shard proofs.
+ /// The address to get the proof for.
+ /// The allowlist proof with full Merkle proof, or null if claimer not found.
+ public static AllowlistProof CalculateMerkleProof(ShardData shardData, string claimerAddress)
+ {
+ if (shardData?.Entries == null || shardData.Entries.Count == 0)
+ {
+ return null;
+ }
+
+ var normalizedAddress = claimerAddress.ToLower();
+
+ // Find the claimer's entry
+ var claimerEntry = shardData.Entries.Find(e => string.Equals(e.Address, normalizedAddress, StringComparison.OrdinalIgnoreCase));
+ if (claimerEntry == null)
+ {
+ return null;
+ }
+
+ // Hash all entries
+ var leaves = new List();
+ var leafToEntryMap = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var entry in shardData.Entries)
+ {
+ var leaf = HashLeaf(entry);
+ leaves.Add(leaf);
+ leafToEntryMap[leaf.BytesToHex()] = entry;
+ }
+
+ // Sort leaves (OpenZeppelin standard)
+ leaves.Sort(CompareBytes);
+
+ // Build tree layers
+ var layers = new List> { leaves };
+ var currentLayer = leaves;
+
+ while (currentLayer.Count > 1)
+ {
+ var nextLayer = new List();
+
+ for (var i = 0; i < currentLayer.Count; i += 2)
+ {
+ if (i + 1 == currentLayer.Count)
+ {
+ // Odd node, promote to next layer
+ nextLayer.Add(currentLayer[i]);
+ }
+ else
+ {
+ // Hash pair
+ nextLayer.Add(HashPair(currentLayer[i], currentLayer[i + 1]));
+ }
+ }
+
+ layers.Add(nextLayer);
+ currentLayer = nextLayer;
+ }
+
+ // Get claimer's leaf
+ var claimerLeaf = HashLeaf(claimerEntry);
+ var claimerLeafHex = claimerLeaf.BytesToHex();
+
+ // Find leaf index in sorted leaves
+ var leafIndex = -1;
+ for (var i = 0; i < leaves.Count; i++)
+ {
+ if (string.Equals(leaves[i].BytesToHex(), claimerLeafHex, StringComparison.OrdinalIgnoreCase))
+ {
+ leafIndex = i;
+ break;
+ }
+ }
+
+ if (leafIndex == -1)
+ {
+ return null;
+ }
+
+ // Build mini-proof
+ var miniProof = new List();
+ var index = leafIndex;
+
+ for (var layerIdx = 0; layerIdx < layers.Count - 1; layerIdx++)
+ {
+ var layer = layers[layerIdx];
+ var isRightNode = index % 2 == 1;
+ var pairIndex = isRightNode ? index - 1 : index + 1;
+
+ if (pairIndex < layer.Count)
+ {
+ miniProof.Add(layer[pairIndex]);
+ }
+
+ index /= 2;
+ }
+
+ // Combine mini-proof with shard proofs
+ var fullProof = new List(miniProof);
+
+ if (shardData.Proofs != null)
+ {
+ foreach (var proofHex in shardData.Proofs)
+ {
+ fullProof.Add(proofHex.HexToBytes());
+ }
+ }
+
+ // Parse entry values for AllowlistProof
+ var maxClaimable = string.IsNullOrEmpty(claimerEntry.MaxClaimable) || string.Equals(claimerEntry.MaxClaimable, "unlimited", StringComparison.OrdinalIgnoreCase)
+ ? _maxUint256
+ : BigInteger.Parse(claimerEntry.MaxClaimable);
+
+ var price = string.IsNullOrEmpty(claimerEntry.Price) || string.Equals(claimerEntry.Price, "unlimited", StringComparison.OrdinalIgnoreCase)
+ ? _maxUint256
+ : BigInteger.Parse(claimerEntry.Price);
+
+ var currency = string.IsNullOrEmpty(claimerEntry.CurrencyAddress)
+ ? Constants.ADDRESS_ZERO
+ : claimerEntry.CurrencyAddress;
+
+ return new AllowlistProof
+ {
+ Proof = fullProof,
+ QuantityLimitPerWallet = maxClaimable,
+ PricePerToken = price,
+ Currency = currency
+ };
+ }
+
+ ///
+ /// Calculates the shard key for a wallet address.
+ ///
+ /// The wallet address.
+ /// The number of hex characters for the shard key (default: 2).
+ /// The shard key (e.g., "c1" for address 0xc143...).
+ public static string GetShardKey(string walletAddress, int shardNybbles = 2)
+ {
+ var normalized = walletAddress.ToLower();
+ if (normalized.StartsWith("0x"))
+ {
+ normalized = normalized[2..];
+ }
+
+ return normalized[..Math.Min(shardNybbles, normalized.Length)];
+ }
+}
diff --git a/global.json b/global.json
index 45334b83..4c4c3ae5 100644
--- a/global.json
+++ b/global.json
@@ -1,7 +1,7 @@
{
"sdk": {
- "version": "8.0.401",
- "rollForward": "latestPatch",
+ "version": "8.0.100",
+ "rollForward": "latestFeature",
"allowPrerelease": false
}
}