Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,132 @@ public class ContractMetadata
/// </summary>
[JsonProperty("image")]
public string Image { get; set; }

/// <summary>
/// Gets or sets the merkle tree mappings for claim conditions.
/// Key: Merkle root hash, Value: IPFS URI to tree info
/// </summary>
[JsonProperty("merkle")]
public Dictionary<string, string> Merkle { get; set; }
}

#endregion

#region Merkle

/// <summary>
/// Represents information about a sharded Merkle tree stored on IPFS.
/// </summary>
public class MerkleTreeInfo
{
/// <summary>
/// Gets or sets the Merkle root hash.
/// </summary>
[JsonProperty("merkleRoot")]
public string MerkleRoot { get; set; }

/// <summary>
/// Gets or sets the base IPFS URI for shard files.
/// </summary>
[JsonProperty("baseUri")]
public string BaseUri { get; set; }

/// <summary>
/// Gets or sets the original entries IPFS URI.
/// </summary>
[JsonProperty("originalEntriesUri")]
public string OriginalEntriesUri { get; set; }

/// <summary>
/// Gets or sets the number of hex characters used for shard keys.
/// </summary>
[JsonProperty("shardNybbles")]
public int ShardNybbles { get; set; } = 2;

/// <summary>
/// Gets or sets the token decimals for price calculations.
/// </summary>
[JsonProperty("tokenDecimals")]
public int TokenDecimals { get; set; } = 0;
}

/// <summary>
/// Represents a shard file containing whitelist entries and proofs.
/// </summary>
public class ShardData
{
/// <summary>
/// Gets or sets the shard proofs (path from shard root to main root).
/// </summary>
[JsonProperty("proofs")]
public List<string> Proofs { get; set; }

/// <summary>
/// Gets or sets the whitelist entries in this shard.
/// </summary>
[JsonProperty("entries")]
public List<WhitelistEntry> Entries { get; set; }
}

/// <summary>
/// Represents a whitelist entry for a claim condition.
/// </summary>
public class WhitelistEntry
{
/// <summary>
/// Gets or sets the wallet address.
/// </summary>
[JsonProperty("address")]
public string Address { get; set; }

/// <summary>
/// Gets or sets the maximum claimable amount ("unlimited" or a number).
/// </summary>
[JsonProperty("maxClaimable")]
public string MaxClaimable { get; set; }

/// <summary>
/// Gets or sets the price override ("unlimited" or a number in wei).
/// </summary>
[JsonProperty("price")]
public string Price { get; set; }

/// <summary>
/// Gets or sets the currency address for price override.
/// </summary>
[JsonProperty("currencyAddress")]
public string CurrencyAddress { get; set; }
}

/// <summary>
/// Represents an allowlist proof for claiming tokens.
/// </summary>
[FunctionOutput]
public class AllowlistProof
{
/// <summary>
/// Gets or sets the Merkle proof (array of bytes32 hashes).
/// </summary>
[Parameter("bytes32[]", "proof", 1)]
public List<byte[]> Proof { get; set; }

/// <summary>
/// Gets or sets the maximum quantity this address can claim.
/// </summary>
[Parameter("uint256", "quantityLimitPerWallet", 2)]
public BigInteger QuantityLimitPerWallet { get; set; }

/// <summary>
/// Gets or sets the price per token override.
/// </summary>
[Parameter("uint256", "pricePerToken", 3)]
public BigInteger PricePerToken { get; set; }

/// <summary>
/// Gets or sets the currency address for the price override.
/// </summary>
[Parameter("address", "currency", 4)]
public string Currency { get; set; }
}

#endregion
Expand Down
228 changes: 222 additions & 6 deletions Thirdweb/Thirdweb.Extensions/ThirdwebExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1555,6 +1555,150 @@ public static async Task<List<NFT>> ERC1155_GetOwnedNFTs(this ThirdwebContract c

#endregion

#region Merkle

/// <summary>
/// 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.
/// </summary>
/// <param name="contract">The drop contract to interact with.</param>
/// <param name="walletAddress">The wallet address to get the proof for.</param>
/// <param name="claimConditionId">Optional claim condition ID. If not provided, uses the active condition.</param>
/// <param name="tokenId">Optional token ID for ERC1155 drops.</param>
/// <returns>The allowlist proof, or null if the wallet is not in the allowlist or it's a public mint.</returns>
/// <exception cref="ArgumentNullException">Thrown when the contract is null.</exception>
/// <exception cref="ArgumentException">Thrown when the wallet address is null or empty.</exception>
public static async Task<AllowlistProof> 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<string>(contract, "contractURI").ConfigureAwait(false);
var metadata = await ThirdwebStorage.Download<ContractMetadata>(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<byte[]>(),
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<Drop_ClaimCondition>(contract, "getClaimConditionById", tokenId.Value, claimConditionId.Value).ConfigureAwait(false);
}
else
{
claimCondition = await ThirdwebContract.Read<Drop_ClaimCondition>(contract, "getClaimConditionById", claimConditionId.Value).ConfigureAwait(false);
}
}
else
{
BigInteger activeId;
if (tokenId.HasValue)
{
activeId = await ThirdwebContract.Read<BigInteger>(contract, "getActiveClaimConditionId", tokenId.Value).ConfigureAwait(false);
claimCondition = await ThirdwebContract.Read<Drop_ClaimCondition>(contract, "getClaimConditionById", tokenId.Value, activeId).ConfigureAwait(false);
}
else
{
activeId = await ThirdwebContract.Read<BigInteger>(contract, "getActiveClaimConditionId").ConfigureAwait(false);
claimCondition = await ThirdwebContract.Read<Drop_ClaimCondition>(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<byte[]>(),
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<MerkleTreeInfo>(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";

ShardData shardData;
try
{
shardData = await ThirdwebStorage.Download<ShardData>(contract.Client, shardUri).ConfigureAwait(false);
}
catch
{
// Try without .json extension
try
{
shardUri = $"{treeInfo.BaseUri}/{shardKey}";
shardData = await ThirdwebStorage.Download<ShardData>(contract.Client, shardUri).ConfigureAwait(false);
}
catch
{
return null; // Shard not found, wallet not in allowlist
}
}

// Calculate proof
return MerkleTreeUtils.CalculateMerkleProof(shardData, walletAddress);
}

#endregion

#region DropERC20

/// <summary>
Expand Down Expand Up @@ -1601,8 +1745,32 @@ public static async Task<ThirdwebTransactionReceipt> DropERC20_Claim(this Thirdw

var payableAmount = isNativeToken ? rawAmountToClaim * activeClaimCondition.PricePerToken / BigInteger.Pow(10, 18) : BigInteger.Zero;

// TODO: Merkle
var allowlistProof = new object[] { Array.Empty<byte>(), 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
var maxUint256 = BigInteger.Parse(Constants.MAX_UINT256_STR);
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<byte[]>(), BigInteger.Zero, BigInteger.Zero, Constants.ADDRESS_ZERO };
}

var fnArgs = new object[]
{
Expand Down Expand Up @@ -1739,8 +1907,32 @@ public static async Task<ThirdwebTransactionReceipt> DropERC721_Claim(this Third

var payableAmount = isNativeToken ? quantity * activeClaimCondition.PricePerToken : BigInteger.Zero;

// TODO: Merkle
var allowlistProof = new object[] { Array.Empty<byte>(), 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
var maxUint256 = BigInteger.Parse(Constants.MAX_UINT256_STR);
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<byte[]>(), BigInteger.Zero, BigInteger.Zero, Constants.ADDRESS_ZERO };
}

var fnArgs = new object[]
{
Expand Down Expand Up @@ -1902,8 +2094,32 @@ public static async Task<ThirdwebTransactionReceipt> DropERC1155_Claim(this Thir

var payableAmount = isNativeToken ? quantity * activeClaimCondition.PricePerToken : BigInteger.Zero;

// TODO: Merkle
var allowlistProof = new object[] { Array.Empty<byte>(), 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
var maxUint256 = BigInteger.Parse(Constants.MAX_UINT256_STR);
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<byte[]>(), BigInteger.Zero, BigInteger.Zero, Constants.ADDRESS_ZERO };
}

var fnArgs = new object[]
{
Expand Down
1 change: 1 addition & 0 deletions Thirdweb/Thirdweb.Utils/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading