diff --git a/.changeset/orange-ravens-remain.md b/.changeset/orange-ravens-remain.md new file mode 100644 index 00000000000..fb119e7e573 --- /dev/null +++ b/.changeset/orange-ravens-remain.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Expose `canClaim` extension function for erc20/721/1155 drops diff --git a/packages/thirdweb/scripts/generate/abis/erc1155/DropERC1155.json b/packages/thirdweb/scripts/generate/abis/erc1155/DropERC1155.json index 182bf7de15f..7111356f668 100644 --- a/packages/thirdweb/scripts/generate/abis/erc1155/DropERC1155.json +++ b/packages/thirdweb/scripts/generate/abis/erc1155/DropERC1155.json @@ -2,5 +2,6 @@ "function setMaxTotalSupply(uint256 _tokenId, uint256 _maxTotalSupply)", "function setSaleRecipientForToken(uint256 _tokenId, address _saleRecipient)", "function updateBatchBaseURI(uint256 _index, string calldata _uri)", - "function freezeBatchBaseURI(uint256 _index)" + "function freezeBatchBaseURI(uint256 _index)", + "function verifyClaim(uint256 _conditionId, address _claimer, uint256 _tokenId, uint256 _quantity, address _currency, uint256 _pricePerToken, (bytes32[] proof, uint256 quantityLimitPerWallet, uint256 pricePerToken, address currency) _allowlistProof) view returns (bool isOverride)" ] \ No newline at end of file diff --git a/packages/thirdweb/scripts/generate/abis/erc20/DropERC20.json b/packages/thirdweb/scripts/generate/abis/erc20/DropERC20.json new file mode 100644 index 00000000000..dc18efc03d8 --- /dev/null +++ b/packages/thirdweb/scripts/generate/abis/erc20/DropERC20.json @@ -0,0 +1,3 @@ +[ + "function verifyClaim(uint256 _conditionId, address _claimer, uint256 _quantity, address _currency, uint256 _pricePerToken, (bytes32[] proof, uint256 quantityLimitPerWallet, uint256 pricePerToken, address currency) _allowlistProof) view returns (bool isOverride)" +] \ No newline at end of file diff --git a/packages/thirdweb/scripts/generate/abis/erc721/DropERC721.json b/packages/thirdweb/scripts/generate/abis/erc721/DropERC721.json index b556dd28c9d..519030525e8 100644 --- a/packages/thirdweb/scripts/generate/abis/erc721/DropERC721.json +++ b/packages/thirdweb/scripts/generate/abis/erc721/DropERC721.json @@ -1,5 +1,6 @@ [ "function updateBatchBaseURI(uint256 _index, string calldata _uri)", "function freezeBatchBaseURI(uint256 _index)", - "function setMaxTotalSupply(uint256 _maxTotalSupply)" + "function setMaxTotalSupply(uint256 _maxTotalSupply)", + "function verifyClaim(uint256 _conditionId, address _claimer, uint256 _quantity, address _currency, uint256 _pricePerToken, (bytes32[] proof, uint256 quantityLimitPerWallet, uint256 pricePerToken, address currency) _allowlistProof) view returns (bool isOverride)" ] \ No newline at end of file diff --git a/packages/thirdweb/src/exports/extensions/erc1155.ts b/packages/thirdweb/src/exports/extensions/erc1155.ts index 4aec0b6b9ce..865d502ae4e 100644 --- a/packages/thirdweb/src/exports/extensions/erc1155.ts +++ b/packages/thirdweb/src/exports/extensions/erc1155.ts @@ -107,6 +107,11 @@ export { type GetClaimConditionsParams, isGetClaimConditionsSupported, } from "../../extensions/erc1155/drops/read/getClaimConditions.js"; +export { + canClaim, + type CanClaimParams, + type CanClaimResult, +} from "../../extensions/erc1155/drops/read/canClaim.js"; // WRITE export { diff --git a/packages/thirdweb/src/exports/extensions/erc20.ts b/packages/thirdweb/src/exports/extensions/erc20.ts index f9965840f20..5f4865ba9e6 100644 --- a/packages/thirdweb/src/exports/extensions/erc20.ts +++ b/packages/thirdweb/src/exports/extensions/erc20.ts @@ -75,6 +75,11 @@ export { getActiveClaimCondition, isGetActiveClaimConditionSupported, } from "../../extensions/erc20/drops/read/getActiveClaimCondition.js"; +export { + canClaim, + type CanClaimParams, + type CanClaimResult, +} from "../../extensions/erc20/drops/read/canClaim.js"; // WRITE export { diff --git a/packages/thirdweb/src/exports/extensions/erc721.ts b/packages/thirdweb/src/exports/extensions/erc721.ts index e229e655469..eda654f7b45 100644 --- a/packages/thirdweb/src/exports/extensions/erc721.ts +++ b/packages/thirdweb/src/exports/extensions/erc721.ts @@ -109,6 +109,11 @@ export { getActiveClaimCondition, isGetActiveClaimConditionSupported, } from "../../extensions/erc721/drops/read/getActiveClaimCondition.js"; +export { + canClaim, + type CanClaimParams, + type CanClaimResult, +} from "../../extensions/erc721/drops/read/canClaim.js"; // WRITE export { diff --git a/packages/thirdweb/src/extensions/erc1155/__generated__/DropERC1155/read/verifyClaim.ts b/packages/thirdweb/src/extensions/erc1155/__generated__/DropERC1155/read/verifyClaim.ts new file mode 100644 index 00000000000..b4467275ef2 --- /dev/null +++ b/packages/thirdweb/src/extensions/erc1155/__generated__/DropERC1155/read/verifyClaim.ts @@ -0,0 +1,223 @@ +import type { AbiParameterToPrimitiveType } from "abitype"; +import { readContract } from "../../../../../transaction/read-contract.js"; +import type { BaseTransactionOptions } from "../../../../../transaction/types.js"; +import { encodeAbiParameters } from "../../../../../utils/abi/encodeAbiParameters.js"; +import { decodeAbiParameters } from "viem"; +import type { Hex } from "../../../../../utils/encoding/hex.js"; +import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js"; + +/** + * Represents the parameters for the "verifyClaim" function. + */ +export type VerifyClaimParams = { + conditionId: AbiParameterToPrimitiveType<{ + type: "uint256"; + name: "_conditionId"; + }>; + claimer: AbiParameterToPrimitiveType<{ type: "address"; name: "_claimer" }>; + tokenId: AbiParameterToPrimitiveType<{ type: "uint256"; name: "_tokenId" }>; + quantity: AbiParameterToPrimitiveType<{ type: "uint256"; name: "_quantity" }>; + currency: AbiParameterToPrimitiveType<{ type: "address"; name: "_currency" }>; + pricePerToken: AbiParameterToPrimitiveType<{ + type: "uint256"; + name: "_pricePerToken"; + }>; + allowlistProof: AbiParameterToPrimitiveType<{ + type: "tuple"; + name: "_allowlistProof"; + components: [ + { type: "bytes32[]"; name: "proof" }, + { type: "uint256"; name: "quantityLimitPerWallet" }, + { type: "uint256"; name: "pricePerToken" }, + { type: "address"; name: "currency" }, + ]; + }>; +}; + +export const FN_SELECTOR = "0xea1def9c" as const; +const FN_INPUTS = [ + { + type: "uint256", + name: "_conditionId", + }, + { + type: "address", + name: "_claimer", + }, + { + type: "uint256", + name: "_tokenId", + }, + { + type: "uint256", + name: "_quantity", + }, + { + type: "address", + name: "_currency", + }, + { + type: "uint256", + name: "_pricePerToken", + }, + { + type: "tuple", + name: "_allowlistProof", + components: [ + { + type: "bytes32[]", + name: "proof", + }, + { + type: "uint256", + name: "quantityLimitPerWallet", + }, + { + type: "uint256", + name: "pricePerToken", + }, + { + type: "address", + name: "currency", + }, + ], + }, +] as const; +const FN_OUTPUTS = [ + { + type: "bool", + name: "isOverride", + }, +] as const; + +/** + * Checks if the `verifyClaim` method is supported by the given contract. + * @param availableSelectors An array of 4byte function selectors of the contract. You can get this in various ways, such as using "whatsabi" or if you have the ABI of the contract available you can use it to generate the selectors. + * @returns A boolean indicating if the `verifyClaim` method is supported. + * @extension ERC1155 + * @example + * ```ts + * import { isVerifyClaimSupported } from "thirdweb/extensions/erc1155"; + * const supported = isVerifyClaimSupported(["0x..."]); + * ``` + */ +export function isVerifyClaimSupported(availableSelectors: string[]) { + return detectMethod({ + availableSelectors, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + }); +} + +/** + * Encodes the parameters for the "verifyClaim" function. + * @param options - The options for the verifyClaim function. + * @returns The encoded ABI parameters. + * @extension ERC1155 + * @example + * ```ts + * import { encodeVerifyClaimParams } from "thirdweb/extensions/erc1155"; + * const result = encodeVerifyClaimParams({ + * conditionId: ..., + * claimer: ..., + * tokenId: ..., + * quantity: ..., + * currency: ..., + * pricePerToken: ..., + * allowlistProof: ..., + * }); + * ``` + */ +export function encodeVerifyClaimParams(options: VerifyClaimParams) { + return encodeAbiParameters(FN_INPUTS, [ + options.conditionId, + options.claimer, + options.tokenId, + options.quantity, + options.currency, + options.pricePerToken, + options.allowlistProof, + ]); +} + +/** + * Encodes the "verifyClaim" function into a Hex string with its parameters. + * @param options - The options for the verifyClaim function. + * @returns The encoded hexadecimal string. + * @extension ERC1155 + * @example + * ```ts + * import { encodeVerifyClaim } from "thirdweb/extensions/erc1155"; + * const result = encodeVerifyClaim({ + * conditionId: ..., + * claimer: ..., + * tokenId: ..., + * quantity: ..., + * currency: ..., + * pricePerToken: ..., + * allowlistProof: ..., + * }); + * ``` + */ +export function encodeVerifyClaim(options: VerifyClaimParams) { + // we do a "manual" concat here to avoid the overhead of the "concatHex" function + // we can do this because we know the specific formats of the values + return (FN_SELECTOR + + encodeVerifyClaimParams(options).slice( + 2, + )) as `${typeof FN_SELECTOR}${string}`; +} + +/** + * Decodes the result of the verifyClaim function call. + * @param result - The hexadecimal result to decode. + * @returns The decoded result as per the FN_OUTPUTS definition. + * @extension ERC1155 + * @example + * ```ts + * import { decodeVerifyClaimResult } from "thirdweb/extensions/erc1155"; + * const result = decodeVerifyClaimResultResult("..."); + * ``` + */ +export function decodeVerifyClaimResult(result: Hex) { + return decodeAbiParameters(FN_OUTPUTS, result)[0]; +} + +/** + * Calls the "verifyClaim" function on the contract. + * @param options - The options for the verifyClaim function. + * @returns The parsed result of the function call. + * @extension ERC1155 + * @example + * ```ts + * import { verifyClaim } from "thirdweb/extensions/erc1155"; + * + * const result = await verifyClaim({ + * contract, + * conditionId: ..., + * claimer: ..., + * tokenId: ..., + * quantity: ..., + * currency: ..., + * pricePerToken: ..., + * allowlistProof: ..., + * }); + * + * ``` + */ +export async function verifyClaim( + options: BaseTransactionOptions, +) { + return readContract({ + contract: options.contract, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + params: [ + options.conditionId, + options.claimer, + options.tokenId, + options.quantity, + options.currency, + options.pricePerToken, + options.allowlistProof, + ], + }); +} diff --git a/packages/thirdweb/src/extensions/erc1155/drop1155.test.ts b/packages/thirdweb/src/extensions/erc1155/drop1155.test.ts index f1bbc35b0f8..6088bac7613 100644 --- a/packages/thirdweb/src/extensions/erc1155/drop1155.test.ts +++ b/packages/thirdweb/src/extensions/erc1155/drop1155.test.ts @@ -22,6 +22,7 @@ import { deployERC1155Contract } from "../prebuilts/deploy-erc1155.js"; import { balanceOf } from "./__generated__/IERC1155/read/balanceOf.js"; import { totalSupply } from "./__generated__/IERC1155/read/totalSupply.js"; import { nextTokenIdToMint } from "./__generated__/IERC1155Enumerable/read/nextTokenIdToMint.js"; +import { canClaim } from "./drops/read/canClaim.js"; import { getActiveClaimCondition } from "./drops/read/getActiveClaimCondition.js"; import { getClaimConditions } from "./drops/read/getClaimConditions.js"; import { claimTo } from "./drops/write/claimTo.js"; @@ -130,6 +131,20 @@ describe.runIf(process.env.TW_SECRET_KEY)( }), account: TEST_ACCOUNT_C, }); + + expect( + await canClaim({ + contract, + claimer: TEST_ACCOUNT_C.address, + quantity: 1n, + tokenId: 0n, + }), + ).toMatchInlineSnapshot(` + { + "result": true, + } + `); + const claimTx = claimTo({ contract, to: TEST_ACCOUNT_C.address, @@ -205,6 +220,33 @@ describe.runIf(process.env.TW_SECRET_KEY)( balanceOf({ contract, owner: TEST_ACCOUNT_B.address, tokenId }), ).resolves.toBe(0n); + expect( + await canClaim({ + contract, + claimer: TEST_ACCOUNT_C.address, + quantity: 1n, + tokenId, + }), + ).toMatchInlineSnapshot(` + { + "result": true, + } + `); + + expect( + await canClaim({ + contract, + claimer: TEST_ACCOUNT_B.address, + quantity: 1n, + tokenId, + }), + ).toMatchInlineSnapshot(` + { + "reason": "DropClaimExceedLimit - 0,1", + "result": false, + } + `); + await sendAndConfirmTransaction({ account: TEST_ACCOUNT_C, transaction: claimTo({ diff --git a/packages/thirdweb/src/extensions/erc1155/drops/read/canClaim.ts b/packages/thirdweb/src/extensions/erc1155/drops/read/canClaim.ts new file mode 100644 index 00000000000..7861f883b60 --- /dev/null +++ b/packages/thirdweb/src/extensions/erc1155/drops/read/canClaim.ts @@ -0,0 +1,57 @@ +import { extractErrorResult } from "../../../../transaction/extract-error.js"; +import type { BaseTransactionOptions } from "../../../../transaction/types.js"; +import { getClaimParams } from "../../../../utils/extensions/drops/get-claim-params.js"; +import { verifyClaim } from "../../__generated__/DropERC1155/read/verifyClaim.js"; +import { getActiveClaimConditionId } from "../../__generated__/IDrop1155/read/getActiveClaimConditionId.js"; + +export type CanClaimParams = { + claimer: string; + quantity: bigint; + tokenId: bigint; + from?: string; +}; + +export type CanClaimResult = { + result: boolean; + reason?: string; +}; + +export async function canClaim( + options: BaseTransactionOptions, +): Promise { + const [conditionId, { quantity, currency, pricePerToken, allowlistProof }] = + await Promise.all([ + getActiveClaimConditionId({ + contract: options.contract, + tokenId: options.tokenId, + }), + getClaimParams({ + contract: options.contract, + quantity: options.quantity, + to: options.claimer, + type: "erc1155", + tokenId: options.tokenId, + from: options.from, + }), + ]); + try { + await verifyClaim({ + contract: options.contract, + claimer: options.claimer, + quantity, + currency, + pricePerToken, + allowlistProof, + conditionId, + tokenId: options.tokenId, + }); + return { + result: true, + }; + } catch (error) { + return { + result: false, + reason: await extractErrorResult({ error, contract: options.contract }), + }; + } +} diff --git a/packages/thirdweb/src/extensions/erc20/__generated__/DropERC20/read/verifyClaim.ts b/packages/thirdweb/src/extensions/erc20/__generated__/DropERC20/read/verifyClaim.ts new file mode 100644 index 00000000000..5033086ff1f --- /dev/null +++ b/packages/thirdweb/src/extensions/erc20/__generated__/DropERC20/read/verifyClaim.ts @@ -0,0 +1,213 @@ +import type { AbiParameterToPrimitiveType } from "abitype"; +import { readContract } from "../../../../../transaction/read-contract.js"; +import type { BaseTransactionOptions } from "../../../../../transaction/types.js"; +import { encodeAbiParameters } from "../../../../../utils/abi/encodeAbiParameters.js"; +import { decodeAbiParameters } from "viem"; +import type { Hex } from "../../../../../utils/encoding/hex.js"; +import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js"; + +/** + * Represents the parameters for the "verifyClaim" function. + */ +export type VerifyClaimParams = { + conditionId: AbiParameterToPrimitiveType<{ + type: "uint256"; + name: "_conditionId"; + }>; + claimer: AbiParameterToPrimitiveType<{ type: "address"; name: "_claimer" }>; + quantity: AbiParameterToPrimitiveType<{ type: "uint256"; name: "_quantity" }>; + currency: AbiParameterToPrimitiveType<{ type: "address"; name: "_currency" }>; + pricePerToken: AbiParameterToPrimitiveType<{ + type: "uint256"; + name: "_pricePerToken"; + }>; + allowlistProof: AbiParameterToPrimitiveType<{ + type: "tuple"; + name: "_allowlistProof"; + components: [ + { type: "bytes32[]"; name: "proof" }, + { type: "uint256"; name: "quantityLimitPerWallet" }, + { type: "uint256"; name: "pricePerToken" }, + { type: "address"; name: "currency" }, + ]; + }>; +}; + +export const FN_SELECTOR = "0x23a2902b" as const; +const FN_INPUTS = [ + { + type: "uint256", + name: "_conditionId", + }, + { + type: "address", + name: "_claimer", + }, + { + type: "uint256", + name: "_quantity", + }, + { + type: "address", + name: "_currency", + }, + { + type: "uint256", + name: "_pricePerToken", + }, + { + type: "tuple", + name: "_allowlistProof", + components: [ + { + type: "bytes32[]", + name: "proof", + }, + { + type: "uint256", + name: "quantityLimitPerWallet", + }, + { + type: "uint256", + name: "pricePerToken", + }, + { + type: "address", + name: "currency", + }, + ], + }, +] as const; +const FN_OUTPUTS = [ + { + type: "bool", + name: "isOverride", + }, +] as const; + +/** + * Checks if the `verifyClaim` method is supported by the given contract. + * @param availableSelectors An array of 4byte function selectors of the contract. You can get this in various ways, such as using "whatsabi" or if you have the ABI of the contract available you can use it to generate the selectors. + * @returns A boolean indicating if the `verifyClaim` method is supported. + * @extension ERC20 + * @example + * ```ts + * import { isVerifyClaimSupported } from "thirdweb/extensions/erc20"; + * const supported = isVerifyClaimSupported(["0x..."]); + * ``` + */ +export function isVerifyClaimSupported(availableSelectors: string[]) { + return detectMethod({ + availableSelectors, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + }); +} + +/** + * Encodes the parameters for the "verifyClaim" function. + * @param options - The options for the verifyClaim function. + * @returns The encoded ABI parameters. + * @extension ERC20 + * @example + * ```ts + * import { encodeVerifyClaimParams } from "thirdweb/extensions/erc20"; + * const result = encodeVerifyClaimParams({ + * conditionId: ..., + * claimer: ..., + * quantity: ..., + * currency: ..., + * pricePerToken: ..., + * allowlistProof: ..., + * }); + * ``` + */ +export function encodeVerifyClaimParams(options: VerifyClaimParams) { + return encodeAbiParameters(FN_INPUTS, [ + options.conditionId, + options.claimer, + options.quantity, + options.currency, + options.pricePerToken, + options.allowlistProof, + ]); +} + +/** + * Encodes the "verifyClaim" function into a Hex string with its parameters. + * @param options - The options for the verifyClaim function. + * @returns The encoded hexadecimal string. + * @extension ERC20 + * @example + * ```ts + * import { encodeVerifyClaim } from "thirdweb/extensions/erc20"; + * const result = encodeVerifyClaim({ + * conditionId: ..., + * claimer: ..., + * quantity: ..., + * currency: ..., + * pricePerToken: ..., + * allowlistProof: ..., + * }); + * ``` + */ +export function encodeVerifyClaim(options: VerifyClaimParams) { + // we do a "manual" concat here to avoid the overhead of the "concatHex" function + // we can do this because we know the specific formats of the values + return (FN_SELECTOR + + encodeVerifyClaimParams(options).slice( + 2, + )) as `${typeof FN_SELECTOR}${string}`; +} + +/** + * Decodes the result of the verifyClaim function call. + * @param result - The hexadecimal result to decode. + * @returns The decoded result as per the FN_OUTPUTS definition. + * @extension ERC20 + * @example + * ```ts + * import { decodeVerifyClaimResult } from "thirdweb/extensions/erc20"; + * const result = decodeVerifyClaimResultResult("..."); + * ``` + */ +export function decodeVerifyClaimResult(result: Hex) { + return decodeAbiParameters(FN_OUTPUTS, result)[0]; +} + +/** + * Calls the "verifyClaim" function on the contract. + * @param options - The options for the verifyClaim function. + * @returns The parsed result of the function call. + * @extension ERC20 + * @example + * ```ts + * import { verifyClaim } from "thirdweb/extensions/erc20"; + * + * const result = await verifyClaim({ + * contract, + * conditionId: ..., + * claimer: ..., + * quantity: ..., + * currency: ..., + * pricePerToken: ..., + * allowlistProof: ..., + * }); + * + * ``` + */ +export async function verifyClaim( + options: BaseTransactionOptions, +) { + return readContract({ + contract: options.contract, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + params: [ + options.conditionId, + options.claimer, + options.quantity, + options.currency, + options.pricePerToken, + options.allowlistProof, + ], + }); +} diff --git a/packages/thirdweb/src/extensions/erc20/drop20.test.ts b/packages/thirdweb/src/extensions/erc20/drop20.test.ts index fbd1c26b7c8..5608b92acd4 100644 --- a/packages/thirdweb/src/extensions/erc20/drop20.test.ts +++ b/packages/thirdweb/src/extensions/erc20/drop20.test.ts @@ -14,6 +14,7 @@ import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value import { toEther } from "../../utils/units.js"; import { name } from "../common/read/name.js"; import { deployERC20Contract } from "../prebuilts/deploy-erc20.js"; +import { canClaim } from "./drops/read/canClaim.js"; import { getClaimConditions } from "./drops/read/getClaimConditions.js"; import { claimTo } from "./drops/write/claimTo.js"; import { resetClaimEligibility } from "./drops/write/resetClaimEligibility.js"; @@ -76,6 +77,18 @@ describe.runIf(process.env.TW_SECRET_KEY)( }), account: TEST_ACCOUNT_A, }); + + expect( + await canClaim({ + contract, + claimer: TEST_ACCOUNT_A.address, + quantity: "1", + }), + ).toMatchInlineSnapshot(` + { + "result": true, + } + `); const claimTx = claimTo({ contract, to: TEST_ACCOUNT_A.address, @@ -180,6 +193,31 @@ describe.runIf(process.env.TW_SECRET_KEY)( } `); + expect( + await canClaim({ + contract, + claimer: TEST_ACCOUNT_A.address, + quantity: "1", + }), + ).toMatchInlineSnapshot(` + { + "result": true, + } + `); + + expect( + await canClaim({ + contract, + claimer: TEST_ACCOUNT_B.address, + quantity: "1", + }), + ).toMatchInlineSnapshot(` + { + "reason": "DropClaimExceedLimit - 0,1000000000000000000", + "result": false, + } + `); + await sendAndConfirmTransaction({ account: TEST_ACCOUNT_A, transaction: claimTo({ diff --git a/packages/thirdweb/src/extensions/erc20/drops/read/canClaim.ts b/packages/thirdweb/src/extensions/erc20/drops/read/canClaim.ts new file mode 100644 index 00000000000..71f65215a9b --- /dev/null +++ b/packages/thirdweb/src/extensions/erc20/drops/read/canClaim.ts @@ -0,0 +1,65 @@ +import { extractErrorResult } from "../../../../transaction/extract-error.js"; +import type { BaseTransactionOptions } from "../../../../transaction/types.js"; +import { getClaimParams } from "../../../../utils/extensions/drops/get-claim-params.js"; +import { verifyClaim } from "../../__generated__/DropERC20/read/verifyClaim.js"; +import { getActiveClaimConditionId } from "../../__generated__/IDropERC20/read/getActiveClaimConditionId.js"; +import { decimals } from "../../read/decimals.js"; + +export type CanClaimParams = { + claimer: string; + from?: string; +} & ({ quantityInWei: bigint } | { quantity: string }); + +export type CanClaimResult = { + result: boolean; + reason?: string; +}; + +export async function canClaim( + options: BaseTransactionOptions, +): Promise { + const quantityWei = await (async () => { + if ("quantityInWei" in options) { + return options.quantityInWei; + } + + const { toUnits } = await import("../../../../utils/units.js"); + return toUnits( + options.quantity, + await decimals({ contract: options.contract }), + ); + })(); + const [conditionId, { quantity, currency, pricePerToken, allowlistProof }] = + await Promise.all([ + getActiveClaimConditionId({ + contract: options.contract, + }), + getClaimParams({ + contract: options.contract, + quantity: quantityWei, + to: options.claimer, + type: "erc20", + from: options.from, + tokenDecimals: await decimals({ contract: options.contract }), + }), + ]); + try { + await verifyClaim({ + contract: options.contract, + claimer: options.claimer, + quantity, + currency, + pricePerToken, + allowlistProof, + conditionId, + }); + return { + result: true, + }; + } catch (error) { + return { + result: false, + reason: await extractErrorResult({ error, contract: options.contract }), + }; + } +} diff --git a/packages/thirdweb/src/extensions/erc721/__generated__/DropERC721/read/verifyClaim.ts b/packages/thirdweb/src/extensions/erc721/__generated__/DropERC721/read/verifyClaim.ts new file mode 100644 index 00000000000..d6fbe26258f --- /dev/null +++ b/packages/thirdweb/src/extensions/erc721/__generated__/DropERC721/read/verifyClaim.ts @@ -0,0 +1,213 @@ +import type { AbiParameterToPrimitiveType } from "abitype"; +import { readContract } from "../../../../../transaction/read-contract.js"; +import type { BaseTransactionOptions } from "../../../../../transaction/types.js"; +import { encodeAbiParameters } from "../../../../../utils/abi/encodeAbiParameters.js"; +import { decodeAbiParameters } from "viem"; +import type { Hex } from "../../../../../utils/encoding/hex.js"; +import { detectMethod } from "../../../../../utils/bytecode/detectExtension.js"; + +/** + * Represents the parameters for the "verifyClaim" function. + */ +export type VerifyClaimParams = { + conditionId: AbiParameterToPrimitiveType<{ + type: "uint256"; + name: "_conditionId"; + }>; + claimer: AbiParameterToPrimitiveType<{ type: "address"; name: "_claimer" }>; + quantity: AbiParameterToPrimitiveType<{ type: "uint256"; name: "_quantity" }>; + currency: AbiParameterToPrimitiveType<{ type: "address"; name: "_currency" }>; + pricePerToken: AbiParameterToPrimitiveType<{ + type: "uint256"; + name: "_pricePerToken"; + }>; + allowlistProof: AbiParameterToPrimitiveType<{ + type: "tuple"; + name: "_allowlistProof"; + components: [ + { type: "bytes32[]"; name: "proof" }, + { type: "uint256"; name: "quantityLimitPerWallet" }, + { type: "uint256"; name: "pricePerToken" }, + { type: "address"; name: "currency" }, + ]; + }>; +}; + +export const FN_SELECTOR = "0x23a2902b" as const; +const FN_INPUTS = [ + { + type: "uint256", + name: "_conditionId", + }, + { + type: "address", + name: "_claimer", + }, + { + type: "uint256", + name: "_quantity", + }, + { + type: "address", + name: "_currency", + }, + { + type: "uint256", + name: "_pricePerToken", + }, + { + type: "tuple", + name: "_allowlistProof", + components: [ + { + type: "bytes32[]", + name: "proof", + }, + { + type: "uint256", + name: "quantityLimitPerWallet", + }, + { + type: "uint256", + name: "pricePerToken", + }, + { + type: "address", + name: "currency", + }, + ], + }, +] as const; +const FN_OUTPUTS = [ + { + type: "bool", + name: "isOverride", + }, +] as const; + +/** + * Checks if the `verifyClaim` method is supported by the given contract. + * @param availableSelectors An array of 4byte function selectors of the contract. You can get this in various ways, such as using "whatsabi" or if you have the ABI of the contract available you can use it to generate the selectors. + * @returns A boolean indicating if the `verifyClaim` method is supported. + * @extension ERC721 + * @example + * ```ts + * import { isVerifyClaimSupported } from "thirdweb/extensions/erc721"; + * const supported = isVerifyClaimSupported(["0x..."]); + * ``` + */ +export function isVerifyClaimSupported(availableSelectors: string[]) { + return detectMethod({ + availableSelectors, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + }); +} + +/** + * Encodes the parameters for the "verifyClaim" function. + * @param options - The options for the verifyClaim function. + * @returns The encoded ABI parameters. + * @extension ERC721 + * @example + * ```ts + * import { encodeVerifyClaimParams } from "thirdweb/extensions/erc721"; + * const result = encodeVerifyClaimParams({ + * conditionId: ..., + * claimer: ..., + * quantity: ..., + * currency: ..., + * pricePerToken: ..., + * allowlistProof: ..., + * }); + * ``` + */ +export function encodeVerifyClaimParams(options: VerifyClaimParams) { + return encodeAbiParameters(FN_INPUTS, [ + options.conditionId, + options.claimer, + options.quantity, + options.currency, + options.pricePerToken, + options.allowlistProof, + ]); +} + +/** + * Encodes the "verifyClaim" function into a Hex string with its parameters. + * @param options - The options for the verifyClaim function. + * @returns The encoded hexadecimal string. + * @extension ERC721 + * @example + * ```ts + * import { encodeVerifyClaim } from "thirdweb/extensions/erc721"; + * const result = encodeVerifyClaim({ + * conditionId: ..., + * claimer: ..., + * quantity: ..., + * currency: ..., + * pricePerToken: ..., + * allowlistProof: ..., + * }); + * ``` + */ +export function encodeVerifyClaim(options: VerifyClaimParams) { + // we do a "manual" concat here to avoid the overhead of the "concatHex" function + // we can do this because we know the specific formats of the values + return (FN_SELECTOR + + encodeVerifyClaimParams(options).slice( + 2, + )) as `${typeof FN_SELECTOR}${string}`; +} + +/** + * Decodes the result of the verifyClaim function call. + * @param result - The hexadecimal result to decode. + * @returns The decoded result as per the FN_OUTPUTS definition. + * @extension ERC721 + * @example + * ```ts + * import { decodeVerifyClaimResult } from "thirdweb/extensions/erc721"; + * const result = decodeVerifyClaimResultResult("..."); + * ``` + */ +export function decodeVerifyClaimResult(result: Hex) { + return decodeAbiParameters(FN_OUTPUTS, result)[0]; +} + +/** + * Calls the "verifyClaim" function on the contract. + * @param options - The options for the verifyClaim function. + * @returns The parsed result of the function call. + * @extension ERC721 + * @example + * ```ts + * import { verifyClaim } from "thirdweb/extensions/erc721"; + * + * const result = await verifyClaim({ + * contract, + * conditionId: ..., + * claimer: ..., + * quantity: ..., + * currency: ..., + * pricePerToken: ..., + * allowlistProof: ..., + * }); + * + * ``` + */ +export async function verifyClaim( + options: BaseTransactionOptions, +) { + return readContract({ + contract: options.contract, + method: [FN_SELECTOR, FN_INPUTS, FN_OUTPUTS] as const, + params: [ + options.conditionId, + options.claimer, + options.quantity, + options.currency, + options.pricePerToken, + options.allowlistProof, + ], + }); +} diff --git a/packages/thirdweb/src/extensions/erc721/drop721.test.ts b/packages/thirdweb/src/extensions/erc721/drop721.test.ts index 115ddb2adaf..d1ce2528651 100644 --- a/packages/thirdweb/src/extensions/erc721/drop721.test.ts +++ b/packages/thirdweb/src/extensions/erc721/drop721.test.ts @@ -25,6 +25,7 @@ import { setClaimConditions } from "./drops/write/setClaimConditions.js"; import { getNFT } from "./read/getNFT.js"; import { lazyMint } from "./write/lazyMint.js"; +import { canClaim } from "./drops/read/canClaim.js"; describe.runIf(process.env.TW_SECRET_KEY)( "DropERC721", { @@ -121,6 +122,19 @@ describe.runIf(process.env.TW_SECRET_KEY)( }), account: TEST_ACCOUNT_A, }); + + expect( + await canClaim({ + contract, + claimer: TEST_ACCOUNT_A.address, + quantity: 1n, + }), + ).toMatchInlineSnapshot(` + { + "result": true, + } + `); + const claimTx = claimTo({ contract, to: TEST_ACCOUNT_A.address, @@ -208,6 +222,31 @@ describe.runIf(process.env.TW_SECRET_KEY)( balanceOf({ contract, owner: TEST_ACCOUNT_B.address }), ).resolves.toBe(0n); + expect( + await canClaim({ + contract, + claimer: TEST_ACCOUNT_A.address, + quantity: 1n, + }), + ).toMatchInlineSnapshot(` + { + "result": true, + } + `); + + expect( + await canClaim({ + contract, + claimer: TEST_ACCOUNT_B.address, + quantity: 1n, + }), + ).toMatchInlineSnapshot(` + { + "reason": "DropClaimExceedLimit - 0,1", + "result": false, + } + `); + await sendAndConfirmTransaction({ account: TEST_ACCOUNT_A, transaction: claimTo({ diff --git a/packages/thirdweb/src/extensions/erc721/drops/read/canClaim.ts b/packages/thirdweb/src/extensions/erc721/drops/read/canClaim.ts new file mode 100644 index 00000000000..f0624d0a059 --- /dev/null +++ b/packages/thirdweb/src/extensions/erc721/drops/read/canClaim.ts @@ -0,0 +1,53 @@ +import { extractErrorResult } from "../../../../transaction/extract-error.js"; +import type { BaseTransactionOptions } from "../../../../transaction/types.js"; +import { getClaimParams } from "../../../../utils/extensions/drops/get-claim-params.js"; +import { verifyClaim } from "../../__generated__/DropERC721/read/verifyClaim.js"; +import { getActiveClaimConditionId } from "../../__generated__/IDrop/read/getActiveClaimConditionId.js"; + +export type CanClaimParams = { + claimer: string; + quantity: bigint; + from?: string; +}; + +export type CanClaimResult = { + result: boolean; + reason?: string; +}; + +export async function canClaim( + options: BaseTransactionOptions, +): Promise { + const [conditionId, { quantity, currency, pricePerToken, allowlistProof }] = + await Promise.all([ + getActiveClaimConditionId({ + contract: options.contract, + }), + getClaimParams({ + contract: options.contract, + quantity: options.quantity, + to: options.claimer, + type: "erc721", + from: options.from, + }), + ]); + try { + await verifyClaim({ + contract: options.contract, + claimer: options.claimer, + quantity, + currency, + pricePerToken, + allowlistProof, + conditionId, + }); + return { + result: true, + }; + } catch (error) { + return { + result: false, + reason: await extractErrorResult({ error, contract: options.contract }), + }; + } +} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatSteps.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatSteps.tsx index e6635bc7f15..5c6ae54f93e 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatSteps.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatSteps.tsx @@ -535,8 +535,7 @@ function PaymentSubStep(props: { {props.icon} - {props.primaryText} - {props.secondaryText} + {props.primaryText} {props.secondaryText} ); diff --git a/packages/thirdweb/src/transaction/actions/gasless/providers/engine.test.ts b/packages/thirdweb/src/transaction/actions/gasless/providers/engine.test.ts index f83632dc250..379e5c2cb8a 100644 --- a/packages/thirdweb/src/transaction/actions/gasless/providers/engine.test.ts +++ b/packages/thirdweb/src/transaction/actions/gasless/providers/engine.test.ts @@ -78,7 +78,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("prepareengineTransaction", () => { { "data": "0xa9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000056bc75e2d63100000", "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 64338n, + "gas": 64340n, "nonce": 0n, "to": "${erc20Contract.address}", "value": 0n, diff --git a/packages/thirdweb/src/transaction/actions/gasless/providers/openzeppelin.test.ts b/packages/thirdweb/src/transaction/actions/gasless/providers/openzeppelin.test.ts index 8c8778e47cf..6a80843fd42 100644 --- a/packages/thirdweb/src/transaction/actions/gasless/providers/openzeppelin.test.ts +++ b/packages/thirdweb/src/transaction/actions/gasless/providers/openzeppelin.test.ts @@ -76,7 +76,7 @@ describe.runIf(process.env.TW_SECRET_KEY)( { "data": "0xa9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000056bc75e2d63100000", "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 64338n, + "gas": 64340n, "nonce": 0n, "to": "${erc20Contract.address}", "value": 0n, diff --git a/packages/thirdweb/src/transaction/extract-error.ts b/packages/thirdweb/src/transaction/extract-error.ts index 1a27f5a9d27..c5c9ec9ddc2 100644 --- a/packages/thirdweb/src/transaction/extract-error.ts +++ b/packages/thirdweb/src/transaction/extract-error.ts @@ -1,9 +1,8 @@ import type { Abi } from "abitype"; -import { type Hex, decodeErrorResult } from "viem"; +import { type Hex, decodeErrorResult, stringify } from "viem"; import { resolveContractAbi } from "../contract/actions/resolve-abi.js"; import type { ThirdwebContract } from "../contract/contract.js"; import { isHex } from "../utils/encoding/hex.js"; -import { stringify } from "../utils/json.js"; import { IS_DEV } from "../utils/process.js"; /** @@ -13,6 +12,18 @@ export async function extractError(args: { error: unknown; contract?: ThirdwebContract; }) { + const { error, contract } = args; + const result = await extractErrorResult({ error, contract }); + if (result) { + return new TransactionError(result, contract); + } + return error; +} + +export async function extractErrorResult(args: { + error: unknown; + contract?: ThirdwebContract; +}): Promise { const { error, contract } = args; if (typeof error === "object") { // try to parse RPC error @@ -31,20 +42,11 @@ export async function extractError(args: { data: errorObj.data, abi, }); - return new TransactionError( - `${parsedError.errorName}${ - parsedError.args ? ` - ${parsedError.args}` : "" - }`, - contract, - ); + return `${parsedError.errorName}${parsedError.args ? ` - ${parsedError.args}` : ""}`; } - return new TransactionError( - `Execution Reverted: ${stringify(errorObj)}`, - contract, - ); } } - return error; + return `Execution Reverted: ${stringify(error)}`; } class TransactionError extends Error {