Skip to content

Commit d65387b

Browse files
committed
[Dashboard] Fix ERC20 claim conditions (#5284)
Fix various issues with ERC20 claim conditions since we up'd it to v5 on the dashboard <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the handling of `tokenDecimals` for ERC20 tokens in the claim conditions form, ensuring that the application correctly manages and displays the maximum claimable supply and per wallet limits based on the token's decimal precision. ### Detailed summary - Updated `MaxClaimableSupplyInput` and `MaxClaimablePerWalletInput` to disable inputs if `tokenDecimals` is not available. - Changed `tokenDecimals` type from `number` to `number | undefined`. - Enhanced fetching logic for `tokenDecimals` in the claim conditions form. - Added error handling for missing `tokenDecimals` when submitting forms. - Introduced conversion functions for handling ERC20 values in wei and display format. - Updated `toBigInt` function to return `bigint | undefined`. - Adjusted `toUnlimited` function to maintain clarity on unlimited values. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 418e3e4 commit d65387b

File tree

5 files changed

+100
-17
lines changed

5 files changed

+100
-17
lines changed

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimablePerWalletInput.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ export const MaxClaimablePerWalletInput: React.FC = () => {
5454
<QuantityInputWithUnlimited
5555
isRequired
5656
decimals={tokenDecimals}
57-
isDisabled={dropType === "specific" || formDisabled}
57+
isDisabled={
58+
dropType === "specific" || formDisabled || (isErc20 && !tokenDecimals)
59+
}
5860
value={field?.maxClaimablePerWallet?.toString() || "0"}
5961
onChange={(value) =>
6062
form.setValue(

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/Inputs/MaxClaimableSupplyInput.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const MaxClaimableSupplyInput: React.FC = () => {
3535
>
3636
<QuantityInputWithUnlimited
3737
isRequired
38-
isDisabled={formDisabled}
38+
isDisabled={formDisabled || (isErc20 && !tokenDecimals)}
3939
decimals={tokenDecimals}
4040
value={field.maxClaimableSupply?.toString() || "0"}
4141
onChange={(value) =>

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/hooks.ts

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
type BaseTransactionOptions,
33
type ThirdwebClient,
44
toTokens,
5+
toUnits,
56
} from "thirdweb";
67
import type { OverrideEntry } from "thirdweb/dist/types/utils/extensions/drops/types";
78
import type { Prettify } from "thirdweb/dist/types/utils/type-utils";
@@ -42,6 +43,7 @@ type CombinedClaimCondition = Prettify<
4243
type Options =
4344
| {
4445
type: "erc20";
46+
decimals?: number;
4547
}
4648
| {
4749
type: "erc721";
@@ -91,12 +93,24 @@ export async function getClaimPhasesInLegacyFormat(
9193
startTime: new Date(Number(condition.startTimestamp * 1000n)),
9294
currencyAddress: condition.currency,
9395
price: condition.pricePerToken,
94-
maxClaimableSupply: toUnlimited(condition.maxClaimableSupply),
96+
maxClaimableSupply:
97+
options.type === "erc20"
98+
? convertERC20ValueToDisplayValue(
99+
condition.maxClaimableSupply,
100+
options.decimals,
101+
)
102+
: toUnlimited(condition.maxClaimableSupply),
95103
currencyMetadata,
96104
currentMintSupply: (
97105
condition.maxClaimableSupply - condition.supplyClaimed
98106
).toString(),
99-
maxClaimablePerWallet: toUnlimited(condition.quantityLimitPerWallet),
107+
maxClaimablePerWallet:
108+
options.type === "erc20"
109+
? convertERC20ValueToDisplayValue(
110+
condition.quantityLimitPerWallet,
111+
options.decimals,
112+
)
113+
: toUnlimited(condition.quantityLimitPerWallet),
100114
merkleRootHash: condition.merkleRoot,
101115
metadata,
102116
snapshot,
@@ -114,8 +128,20 @@ export function setClaimPhasesTx(
114128
const phases = rawPhases.map((phase) => {
115129
return {
116130
startTime: toDate(phase.startTime),
117-
maxClaimableSupply: toBigInt(phase.maxClaimableSupply),
118-
maxClaimablePerWallet: toBigInt(phase.maxClaimablePerWallet),
131+
maxClaimableSupply:
132+
baseOptions.type === "erc20"
133+
? convertERC20ValueToWei(
134+
phase.maxClaimableSupply,
135+
baseOptions.decimals,
136+
)
137+
: toBigInt(phase.maxClaimableSupply),
138+
maxClaimablePerWallet:
139+
baseOptions.type === "erc20"
140+
? convertERC20ValueToWei(
141+
phase.maxClaimablePerWallet,
142+
baseOptions.decimals,
143+
)
144+
: toBigInt(phase.maxClaimablePerWallet),
119145
merkleRootHash: phase.merkleRootHash as string | undefined,
120146
overrideList: phase.snapshot?.length
121147
? snapshotToOverrides(phase.snapshot)
@@ -175,18 +201,56 @@ function toDate(timestamp: number | Date | undefined) {
175201
}
176202
return new Date(timestamp);
177203
}
178-
function toBigInt(value: string | number | undefined) {
204+
function toBigInt(value: string | number | undefined): bigint | undefined {
179205
if (value === undefined) {
180206
return undefined;
181207
}
182208
if (value === "unlimited") {
183209
return maxUint256;
184210
}
211+
}
212+
213+
// The input from client-side is non-wei, but the extension is expecting value in wei
214+
// so we need to convert it using toUnits
215+
function convertERC20ValueToWei(
216+
value: string | number | undefined,
217+
decimals?: number,
218+
) {
219+
if (value === undefined) {
220+
return undefined;
221+
}
222+
if (value === "unlimited") {
223+
return maxUint256;
224+
}
225+
// The ERC20Claim condition extension in v5 does not convert to wei for us
226+
// so we have to, manually
227+
if (decimals) {
228+
return toUnits(value.toString(), decimals);
229+
}
185230
return BigInt(value);
186231
}
187232

233+
// This value we get from ERC20Ext.getClaimConditions is in wei
234+
// so we have to convert it using toTokens for readability, and for users to update
235+
// (when user updates this value, we convert it back to wei - see `function setClaimPhasesTx`)
236+
function convertERC20ValueToDisplayValue(
237+
value: bigint,
238+
decimals?: number,
239+
): string {
240+
if (value === maxUint256) {
241+
return "unlimited";
242+
}
243+
if (decimals) {
244+
return toTokens(value, decimals);
245+
}
246+
return value.toString();
247+
}
248+
188249
function toUnlimited(value: bigint) {
189-
return value === maxUint256 ? "unlimited" : value.toString();
250+
if (value === maxUint256) {
251+
return "unlimited";
252+
}
253+
return value.toString();
190254
}
191255

192256
async function fetchSnapshot(

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
useFieldArray,
2727
useForm,
2828
} from "react-hook-form";
29+
import { toast } from "sonner";
2930
import {
3031
NATIVE_TOKEN_ADDRESS,
3132
type ThirdwebContract,
@@ -152,7 +153,7 @@ interface ClaimsConditionFormContextData {
152153
field: ControlledField;
153154
phaseIndex: number;
154155
formDisabled: boolean;
155-
tokenDecimals: number;
156+
tokenDecimals: number | undefined;
156157
isMultiPhase: boolean;
157158
isActive: boolean;
158159
dropType: DropType;
@@ -210,7 +211,6 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
210211
enabled: isErc20,
211212
},
212213
});
213-
const tokenDecimalsData = tokenDecimals.data ?? 0;
214214
const saveClaimPhaseNotification = useTxNotifications(
215215
"Saved claim phases",
216216
"Failed to save claim phases",
@@ -219,7 +219,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
219219
const claimConditionsQuery = useReadContract(getClaimPhasesInLegacyFormat, {
220220
contract,
221221
...(isErc20
222-
? { type: "erc20" }
222+
? { type: "erc20", decimals: tokenDecimals.data }
223223
: isErc721
224224
? { type: "erc721" }
225225
: { type: "erc1155", tokenId: BigInt(tokenId || 0) }),
@@ -259,7 +259,11 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
259259
);
260260
}, [claimConditionsQuery.data, isMultiPhase]);
261261

262-
const isFetchingData = claimConditionsQuery.isFetching || sendTx.isPending;
262+
const isFetchingData =
263+
claimConditionsQuery.isFetching ||
264+
sendTx.isPending ||
265+
// Need to make sure the tokenDecimals.data is present when interacting with ERC20 claim conditions
266+
(isErc20 && tokenDecimals.isLoading);
263267

264268
const canEditForm = isAdmin && !isFetchingData;
265269

@@ -353,13 +357,17 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
353357
action: "set-claim-conditions",
354358
label: "attempt",
355359
});
356-
360+
if (isErc20 && !tokenDecimals.data) {
361+
return toast.error(
362+
`Could not fetch token decimals for contract ${contract.address}`,
363+
);
364+
}
357365
try {
358366
const tx = setClaimPhasesTx(
359367
{
360368
contract,
361369
...(isErc20
362-
? { type: "erc20" }
370+
? { type: "erc20", decimals: tokenDecimals.data }
363371
: isErc721
364372
? { type: "erc721" }
365373
: { type: "erc1155", tokenId: BigInt(tokenId || 0) }),
@@ -453,6 +461,15 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
453461
);
454462
}
455463

464+
// Do not proceed if fails to load the tokenDecimals.data - for ERC20 drop contracts specifically
465+
if (isErc20 && tokenDecimals.data === undefined) {
466+
return (
467+
<div className="flex h-[400px] w-full items-center justify-center rounded-lg border border-border">
468+
Failed to load token decimals
469+
</div>
470+
);
471+
}
472+
456473
return (
457474
<>
458475
<Flex onSubmit={handleFormSubmit} direction="column" as="form" gap={10}>
@@ -508,7 +525,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
508525
phaseIndex: index,
509526
formDisabled: !canEditForm,
510527
isErc20,
511-
tokenDecimals: tokenDecimalsData,
528+
tokenDecimals: tokenDecimals.data,
512529
dropType,
513530
setOpenSnapshotIndex,
514531
isAdmin,

apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/quantity-input-with-unlimited.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const QuantityInputWithUnlimited: React.FC<
5353
<InputGroup {...restInputProps}>
5454
<Input
5555
isRequired={isRequired}
56-
isDisabled={decimals === undefined || isDisabled}
56+
isDisabled={isDisabled}
5757
value={stringValue === "unlimited" ? "Unlimited" : stringValue}
5858
onChange={(e) => updateValue(e.currentTarget.value)}
5959
onBlur={() => {
@@ -69,7 +69,7 @@ export const QuantityInputWithUnlimited: React.FC<
6969
{hideMaxButton ? null : (
7070
<InputRightElement w="auto">
7171
<Button
72-
isDisabled={decimals === undefined || isDisabled}
72+
isDisabled={isDisabled}
7373
colorScheme="primary"
7474
variant="ghost"
7575
size="sm"

0 commit comments

Comments
 (0)