diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimablePerWalletInput.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimablePerWalletInput.tsx index 2f73977679f..fb20e0dfa6c 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimablePerWalletInput.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimablePerWalletInput.tsx @@ -54,7 +54,9 @@ export const MaxClaimablePerWalletInput: React.FC = () => { form.setValue( diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimableSupplyInput.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimableSupplyInput.tsx index c550ede286f..be2c3b57458 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimableSupplyInput.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimableSupplyInput.tsx @@ -35,7 +35,7 @@ export const MaxClaimableSupplyInput: React.FC = () => { > diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/hooks.ts b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/hooks.ts index 80c02dd8cfa..f66c4f495b3 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/hooks.ts +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/hooks.ts @@ -2,6 +2,7 @@ import { type BaseTransactionOptions, type ThirdwebClient, toTokens, + toUnits, } from "thirdweb"; import type { OverrideEntry } from "thirdweb/dist/types/utils/extensions/drops/types"; import type { Prettify } from "thirdweb/dist/types/utils/type-utils"; @@ -42,6 +43,7 @@ type CombinedClaimCondition = Prettify< type Options = | { type: "erc20"; + decimals?: number; } | { type: "erc721"; @@ -91,12 +93,24 @@ export async function getClaimPhasesInLegacyFormat( startTime: new Date(Number(condition.startTimestamp * 1000n)), currencyAddress: condition.currency, price: condition.pricePerToken, - maxClaimableSupply: toUnlimited(condition.maxClaimableSupply), + maxClaimableSupply: + options.type === "erc20" + ? convertERC20ValueToDisplayValue( + condition.maxClaimableSupply, + options.decimals, + ) + : toUnlimited(condition.maxClaimableSupply), currencyMetadata, currentMintSupply: ( condition.maxClaimableSupply - condition.supplyClaimed ).toString(), - maxClaimablePerWallet: toUnlimited(condition.quantityLimitPerWallet), + maxClaimablePerWallet: + options.type === "erc20" + ? convertERC20ValueToDisplayValue( + condition.quantityLimitPerWallet, + options.decimals, + ) + : toUnlimited(condition.quantityLimitPerWallet), merkleRootHash: condition.merkleRoot, metadata, snapshot, @@ -114,8 +128,20 @@ export function setClaimPhasesTx( const phases = rawPhases.map((phase) => { return { startTime: toDate(phase.startTime), - maxClaimableSupply: toBigInt(phase.maxClaimableSupply), - maxClaimablePerWallet: toBigInt(phase.maxClaimablePerWallet), + maxClaimableSupply: + baseOptions.type === "erc20" + ? convertERC20ValueToWei( + phase.maxClaimableSupply, + baseOptions.decimals, + ) + : toBigInt(phase.maxClaimableSupply), + maxClaimablePerWallet: + baseOptions.type === "erc20" + ? convertERC20ValueToWei( + phase.maxClaimablePerWallet, + baseOptions.decimals, + ) + : toBigInt(phase.maxClaimablePerWallet), merkleRootHash: phase.merkleRootHash as string | undefined, overrideList: phase.snapshot?.length ? snapshotToOverrides(phase.snapshot) @@ -175,18 +201,56 @@ function toDate(timestamp: number | Date | undefined) { } return new Date(timestamp); } -function toBigInt(value: string | number | undefined) { +function toBigInt(value: string | number | undefined): bigint | undefined { if (value === undefined) { return undefined; } if (value === "unlimited") { return maxUint256; } +} + +// The input from client-side is non-wei, but the extension is expecting value in wei +// so we need to convert it using toUnits +function convertERC20ValueToWei( + value: string | number | undefined, + decimals?: number, +) { + if (value === undefined) { + return undefined; + } + if (value === "unlimited") { + return maxUint256; + } + // The ERC20Claim condition extension in v5 does not convert to wei for us + // so we have to, manually + if (decimals) { + return toUnits(value.toString(), decimals); + } return BigInt(value); } +// This value we get from ERC20Ext.getClaimConditions is in wei +// so we have to convert it using toTokens for readability, and for users to update +// (when user updates this value, we convert it back to wei - see `function setClaimPhasesTx`) +function convertERC20ValueToDisplayValue( + value: bigint, + decimals?: number, +): string { + if (value === maxUint256) { + return "unlimited"; + } + if (decimals) { + return toTokens(value, decimals); + } + return value.toString(); +} + function toUnlimited(value: bigint) { - return value === maxUint256 ? "unlimited" : value.toString(); + if (value === maxUint256) { + return "unlimited"; + } + return value.toString(); } async function fetchSnapshot( diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx index 791fc990776..0b679895827 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx @@ -26,6 +26,7 @@ import { useFieldArray, useForm, } from "react-hook-form"; +import { toast } from "sonner"; import { NATIVE_TOKEN_ADDRESS, type ThirdwebContract, @@ -152,7 +153,7 @@ interface ClaimsConditionFormContextData { field: ControlledField; phaseIndex: number; formDisabled: boolean; - tokenDecimals: number; + tokenDecimals: number | undefined; isMultiPhase: boolean; isActive: boolean; dropType: DropType; @@ -210,7 +211,6 @@ export const ClaimConditionsForm: React.FC = ({ enabled: isErc20, }, }); - const tokenDecimalsData = tokenDecimals.data ?? 0; const saveClaimPhaseNotification = useTxNotifications( "Saved claim phases", "Failed to save claim phases", @@ -219,7 +219,7 @@ export const ClaimConditionsForm: React.FC = ({ const claimConditionsQuery = useReadContract(getClaimPhasesInLegacyFormat, { contract, ...(isErc20 - ? { type: "erc20" } + ? { type: "erc20", decimals: tokenDecimals.data } : isErc721 ? { type: "erc721" } : { type: "erc1155", tokenId: BigInt(tokenId || 0) }), @@ -259,7 +259,11 @@ export const ClaimConditionsForm: React.FC = ({ ); }, [claimConditionsQuery.data, isMultiPhase]); - const isFetchingData = claimConditionsQuery.isFetching || sendTx.isPending; + const isFetchingData = + claimConditionsQuery.isFetching || + sendTx.isPending || + // Need to make sure the tokenDecimals.data is present when interacting with ERC20 claim conditions + (isErc20 && tokenDecimals.isLoading); const canEditForm = isAdmin && !isFetchingData; @@ -353,13 +357,17 @@ export const ClaimConditionsForm: React.FC = ({ action: "set-claim-conditions", label: "attempt", }); - + if (isErc20 && !tokenDecimals.data) { + return toast.error( + `Could not fetch token decimals for contract ${contract.address}`, + ); + } try { const tx = setClaimPhasesTx( { contract, ...(isErc20 - ? { type: "erc20" } + ? { type: "erc20", decimals: tokenDecimals.data } : isErc721 ? { type: "erc721" } : { type: "erc1155", tokenId: BigInt(tokenId || 0) }), @@ -453,6 +461,15 @@ export const ClaimConditionsForm: React.FC = ({ ); } + // Do not proceed if fails to load the tokenDecimals.data - for ERC20 drop contracts specifically + if (isErc20 && tokenDecimals.data === undefined) { + return ( +
+ Failed to load token decimals +
+ ); + } + return ( <> @@ -508,7 +525,7 @@ export const ClaimConditionsForm: React.FC = ({ phaseIndex: index, formDisabled: !canEditForm, isErc20, - tokenDecimals: tokenDecimalsData, + tokenDecimals: tokenDecimals.data, dropType, setOpenSnapshotIndex, isAdmin, diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/quantity-input-with-unlimited.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/quantity-input-with-unlimited.tsx index 96888dc6966..af7bc006abb 100644 --- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/quantity-input-with-unlimited.tsx +++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/quantity-input-with-unlimited.tsx @@ -53,7 +53,7 @@ export const QuantityInputWithUnlimited: React.FC< updateValue(e.currentTarget.value)} onBlur={() => { @@ -69,7 +69,7 @@ export const QuantityInputWithUnlimited: React.FC< {hideMaxButton ? null : (