remove claimconditions legacy (#4590)

## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to refactor claim conditions components and improve error handling.

### Detailed summary
- Refactored components related to claim conditions
- Improved error handling in `errorParser.tsx`
- Updated transaction button component
- Enhanced ERC20 amount conversion logic
- Streamlined wallet confirmation logic

> The following files were skipped due to too many changes: `apps/dashboard/src/contract-ui/tabs/claim-conditions/components/snapshot-upload.tsx`, `apps/dashboard/src/contract-ui/tabs/claim-conditions/components/claim-conditions-form/hooks.ts`, `apps/dashboard/src/contract-ui/tabs/claim-conditions/components/claim-conditions-form/index.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
This commit is contained in:
jnsdls
2024-09-15 03:49:04 +00:00
parent ebe4975aa3
commit 390aedcc8a
15 changed files with 503 additions and 368 deletions
@@ -35,7 +35,6 @@ interface TransactionButtonProps extends Omit<ButtonProps, "leftIcon"> {
isLoading: boolean;
isGasless?: boolean;
upsellTestnet?: boolean;
onChainSelect?: (chainId: number) => void;
}
function useWalletRequiresExternalConfirmation() {
@@ -55,7 +54,7 @@ export const TransactionButton: React.FC<TransactionButtonProps> = ({
colorScheme,
variant,
isGasless,
onChainSelect,
...restButtonProps
}) => {
const activeWallet = useActiveWallet();
@@ -93,7 +92,6 @@ export const TransactionButton: React.FC<TransactionButtonProps> = ({
>
<PopoverTrigger>
<ButtonComponent
onChainSelect={onChainSelect}
borderRadius="md"
position="relative"
role="group"
@@ -17,10 +17,9 @@ export const ClaimPriceInput = () => {
field,
isColumn,
claimConditionType,
isClaimPhaseV1,
} = useClaimConditionsFormContext();
if (!isClaimPhaseV1 && claimConditionType === "creator") {
if (claimConditionType === "creator") {
return null;
}
@@ -18,7 +18,6 @@ export const ClaimerSelection = () => {
field,
dropType,
isErc20,
isClaimPhaseV1,
setOpenSnapshotIndex: setOpenIndex,
isAdmin,
isColumn,
@@ -31,14 +30,10 @@ export const ClaimerSelection = () => {
if (val === "any") {
form.setValue(`phases.${phaseIndex}.snapshot`, undefined);
} else {
if (val === "specific" && !isClaimPhaseV1) {
if (val === "specific") {
form.setValue(`phases.${phaseIndex}.maxClaimablePerWallet`, 0);
}
if (
val === "overrides" &&
!isClaimPhaseV1 &&
field.maxClaimablePerWallet !== 1
) {
if (val === "overrides" && field.maxClaimablePerWallet !== 1) {
form.setValue(`phases.${phaseIndex}.maxClaimablePerWallet`, 1);
}
form.setValue(`phases.${phaseIndex}.snapshot`, []);
@@ -50,39 +45,31 @@ export const ClaimerSelection = () => {
const disabledSnapshotButton = isAdmin && formDisabled;
if (isClaimPhaseV1) {
helperText =
"Snapshot spots are one-time-use! Once a wallet has claimed the drop, it cannot claim again, even if it did not claim the entire amount assigned to it in the snapshot.";
if (dropType === "specific") {
helperText = (
<>
<b>Only</b> wallets on the <b>allowlist</b> can claim.
</>
);
} else if (dropType === "any") {
helperText = (
<>
<b>Anyone</b> can claim based on the rules defined in this phase.
(&quot;Public Mint&quot;)
</>
);
} else {
if (dropType === "specific") {
helperText = (
<>
<b>Only</b> wallets on the <b>allowlist</b> can claim.
</>
);
} else if (dropType === "any") {
helperText = (
<>
<b>Anyone</b> can claim based on the rules defined in this phase.
(&quot;Public Mint&quot;)
</>
);
} else {
helperText = (
<>
<b>Anyone</b> can claim based on the rules defined in this phase.
<br />
<b>Wallets in the snapshot</b> can claim with special rules defined in
the snapshot.
</>
);
}
helperText = (
<>
<b>Anyone</b> can claim based on the rules defined in this phase.
<br />
<b>Wallets in the snapshot</b> can claim with special rules defined in
the snapshot.
</>
);
}
if (
!isClaimPhaseV1 &&
(claimConditionType === "public" || claimConditionType === "creator")
) {
if (claimConditionType === "public" || claimConditionType === "creator") {
return null;
}
@@ -113,9 +100,7 @@ export const ClaimerSelection = () => {
onChange={handleClaimerChange}
>
<option value="any">Any wallet</option>
{!isClaimPhaseV1 ? (
<option value="overrides">Any wallet (with overrides)</option>
) : null}
<option value="overrides">Any wallet (with overrides)</option>
<option value="specific">Only specific wallets</option>
</Select>
)}
@@ -10,7 +10,6 @@ export const MaxClaimablePerWalletInput: React.FC = () => {
const {
formDisabled,
isErc20,
isClaimPhaseV1,
form,
tokenDecimals,
field,
@@ -26,9 +25,7 @@ export const MaxClaimablePerWalletInput: React.FC = () => {
return (
<CustomFormControl
disabled={formDisabled}
label={`How many ${isErc20 ? "tokens" : "NFTs"} can be claimed per ${
isClaimPhaseV1 ? "transaction" : "wallet"
}?`}
label={`How many ${isErc20 ? "tokens" : "NFTs"} can be claimed per wallet?`}
error={
form.getFieldState(
`phases.${phaseIndex}.maxClaimablePerWallet`,
@@ -37,14 +34,10 @@ export const MaxClaimablePerWalletInput: React.FC = () => {
}
helperText={
<>
{!isClaimPhaseV1 ? (
<>
This value applies for <strong>all</strong> wallets
{claimConditionType !== "public"
? ", and can be overridden for specific wallets in the snapshot. "
: ". "}
</>
) : null}
This value applies for <strong>all</strong> wallets
{claimConditionType !== "public"
? ", and can be overridden for specific wallets in the snapshot. "
: ". "}
Limits are set per wallets and not per user, sophisticated actors
could get around wallet restrictions.{" "}
<Link
@@ -61,9 +54,7 @@ export const MaxClaimablePerWalletInput: React.FC = () => {
<QuantityInputWithUnlimited
isRequired
decimals={tokenDecimals}
isDisabled={
(!isClaimPhaseV1 && dropType === "specific") || formDisabled
}
isDisabled={dropType === "specific" || formDisabled}
value={field?.maxClaimablePerWallet?.toString() || "0"}
onChange={(value) =>
form.setValue(
@@ -14,10 +14,9 @@ export const MaxClaimableSupplyInput: React.FC = () => {
tokenDecimals,
field,
claimConditionType,
isClaimPhaseV1,
} = useClaimConditionsFormContext();
if (!isClaimPhaseV1 && claimConditionType === "creator") {
if (claimConditionType === "creator") {
return null;
}
@@ -6,15 +6,11 @@ import { useClaimConditionsFormContext } from "../index";
* Allows users to edit the name for the claim phase.
*/
export const PhaseNameInput: React.FC = () => {
const { phaseIndex, formDisabled, isClaimPhaseV1, form, field } =
const { phaseIndex, formDisabled, form, field } =
useClaimConditionsFormContext();
const inputPlaceholder = `Phase ${phaseIndex + 1}`;
const inputValue = field.metadata?.name;
if (isClaimPhaseV1) {
return null;
}
return (
<CustomFormControl
disabled={formDisabled}
@@ -0,0 +1,237 @@
import type {
ClaimCondition as LegacyClaimCondition,
ClaimConditionInputSchema as LegacyClaimConditionInputSchema,
} from "@thirdweb-dev/sdk";
import {
type BaseTransactionOptions,
type ThirdwebClient,
toTokens,
} from "thirdweb";
import type { OverrideEntry } from "thirdweb/dist/types/utils/extensions/drops/types";
import type { Prettify } from "thirdweb/dist/types/utils/type-utils";
import { getContractMetadata } from "thirdweb/extensions/common";
import * as ERC20Ext from "thirdweb/extensions/erc20";
import * as ERC721Ext from "thirdweb/extensions/erc721";
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
import { download } from "thirdweb/storage";
import { maxUint256 } from "thirdweb/utils";
import type { z } from "zod";
type SnapshotEntry = {
address: string;
maxClaimable: string;
price?: string | undefined;
currencyAddress?: string | undefined;
};
export type CombinedClaimCondition = Prettify<
Omit<
LegacyClaimCondition,
"price" | "waitInSeconds" | "availableSupply" | "currencyMetadata"
> & {
price: bigint;
currencyMetadata: Omit<
LegacyClaimCondition["currencyMetadata"],
"value"
> & {
value: bigint;
};
}
>;
type Options =
| {
type: "erc20";
}
| {
type: "erc721";
}
| {
type: "erc1155";
tokenId: bigint;
};
export async function getClaimPhasesInLegacyFormat(
options: BaseTransactionOptions<Options>,
): Promise<CombinedClaimCondition[]> {
const conditions = await (async () => {
switch (options.type) {
case "erc20":
return ERC20Ext.getClaimConditions(options);
case "erc721":
return ERC721Ext.getClaimConditions(options);
case "erc1155":
return ERC1155Ext.getClaimConditions(options);
}
})();
return Promise.all(
conditions.map(async (condition) => {
const [currencyMetadata, metadata, contractMetadata] = await Promise.all([
await ERC20Ext.getCurrencyMetadata({
contract: { ...options.contract, address: condition.currency },
}).then((m) => ({
...m,
displayValue: toTokens(condition.pricePerToken, m.decimals),
value: condition.pricePerToken,
})),
download({
uri: condition.metadata,
client: options.contract.client,
}).then((r) => r.json()),
getContractMetadata(options),
]);
const snapshot = await fetchSnapshot(
condition.merkleRoot,
contractMetadata.merkle,
options.contract.client,
);
return {
...condition,
startTime: new Date(Number(condition.startTimestamp * 1000n)),
currencyAddress: condition.currency,
price: condition.pricePerToken,
maxClaimableSupply: toUnlimited(condition.maxClaimableSupply),
currencyMetadata,
currentMintSupply: (
condition.maxClaimableSupply - condition.supplyClaimed
).toString(),
maxClaimablePerWallet: toUnlimited(condition.quantityLimitPerWallet),
merkleRootHash: condition.merkleRoot,
metadata,
snapshot,
} satisfies CombinedClaimCondition;
}),
);
}
type PhaseInput = z.input<typeof LegacyClaimConditionInputSchema>;
export function setClaimPhasesTx(
baseOptions: BaseTransactionOptions<Options>,
rawPhases: PhaseInput[],
) {
const phases = rawPhases.map((phase) => {
return {
startTime: toDate(phase.startTime),
maxClaimableSupply: toBigInt(phase.maxClaimableSupply),
maxClaimablePerWallet: toBigInt(phase.maxClaimablePerWallet),
merkleRootHash: phase.merkleRootHash as string | undefined,
overrideList: phase.snapshot?.length
? snapshotToOverrides(phase.snapshot)
: undefined,
price: phase.price?.toString(),
currencyAddress: phase.currencyAddress,
metadata: phase.metadata,
} satisfies ERC20Ext.SetClaimConditionsParams["phases"][0];
});
switch (baseOptions.type) {
case "erc20":
return ERC20Ext.setClaimConditions({
contract: baseOptions.contract,
phases,
});
case "erc721":
return ERC721Ext.setClaimConditions({
contract: baseOptions.contract,
phases,
});
case "erc1155":
return ERC1155Ext.setClaimConditions({
contract: baseOptions.contract,
phases,
tokenId: baseOptions.tokenId,
});
}
}
// HELPERS
function snapshotToOverrides(snapshot: PhaseInput["snapshot"]) {
if (!snapshot) {
return [];
}
return snapshot.map((entry) => {
if (typeof entry === "string") {
return {
address: entry,
} satisfies OverrideEntry;
}
return {
address: entry.address,
maxClaimable: entry.maxClaimable?.toString(),
price: entry.price?.toString(),
currencyAddress: entry.currencyAddress,
} satisfies OverrideEntry;
});
}
function toDate(timestamp: number | Date | undefined) {
if (timestamp === undefined) {
return undefined;
}
if (timestamp instanceof Date) {
return timestamp;
}
return new Date(timestamp);
}
function toBigInt(value: string | number | undefined) {
if (value === undefined) {
return undefined;
}
if (value === "unlimited") {
return maxUint256;
}
return BigInt(value);
}
function toUnlimited(value: bigint) {
return value === maxUint256 ? "unlimited" : value.toString();
}
async function fetchSnapshot(
merkleRoot: string,
merkleMetadata: Record<string, string> | undefined,
client: ThirdwebClient,
): Promise<SnapshotEntry[] | null> {
if (!merkleMetadata) {
return null;
}
try {
const snapshotUri = merkleMetadata[merkleRoot];
if (snapshotUri) {
const raw = await download({
uri: snapshotUri,
client: client,
}).then((r) => r.json());
if (raw.isShardedMerkleTree && raw.merkleRoot === merkleRoot) {
return download({
uri: raw.originalEntriesUri,
client: client,
})
.then((r) => r.json())
.catch(() => null);
}
if (merkleRoot === raw.merkleRoot) {
return raw.claims.map(
(claim: {
address: string;
maxClaimable: bigint;
price: bigint;
currencyAddress: string;
}) => ({
address: claim.address,
maxClaimable: claim.maxClaimable,
price: claim.price,
currencyAddress: claim.currencyAddress,
}),
);
}
}
return null;
} catch (e) {
console.error("failed to fetch snapshot", e);
return null;
}
}
@@ -13,11 +13,6 @@ import {
MenuList,
Spinner,
} from "@chakra-ui/react";
import {
type DropContract,
useClaimConditions,
useSetClaimConditions,
} from "@thirdweb-dev/react";
import {
type ClaimConditionInput,
ClaimConditionInputSchema,
@@ -42,12 +37,17 @@ import {
ZERO_ADDRESS,
} from "thirdweb";
import { decimals } from "thirdweb/extensions/erc20";
import { useActiveAccount, useReadContract } from "thirdweb/react";
import {
useActiveAccount,
useReadContract,
useSendAndConfirmTransaction,
} from "thirdweb/react";
import invariant from "tiny-invariant";
import { Button, Heading, MenuItem, Text } from "tw-components";
import * as z from "zod";
import { ZodError } from "zod";
import { ResetClaimEligibility } from "../reset-claim-eligibility";
import { getClaimPhasesInLegacyFormat, setClaimPhasesTx } from "./hooks";
import { ClaimConditionsPhase } from "./phase";
type ClaimConditionDashboardInput = ClaimConditionInput & {
@@ -59,7 +59,6 @@ const DEFAULT_PHASE: ClaimConditionDashboardInput = {
startTime: new Date(),
maxClaimableSupply: "unlimited",
maxClaimablePerWallet: "unlimited",
waitInSeconds: "0",
price: "0",
currencyAddress: NATIVE_TOKEN_ADDRESS,
snapshot: undefined,
@@ -153,7 +152,6 @@ interface ClaimsConditionFormContextData {
phaseIndex: number;
formDisabled: boolean;
tokenDecimals: number;
isClaimPhaseV1: boolean;
isMultiPhase: boolean;
isActive: boolean;
dropType: DropType;
@@ -177,14 +175,10 @@ export function useClaimConditionsFormContext() {
return data;
}
// TODO: refactor, this is no longer supported
const isClaimPhaseV1 = false;
interface ClaimConditionsFormProps {
contract: DropContract;
contract: ThirdwebContract;
tokenId?: string;
isColumn?: true;
contractV5: ThirdwebContract;
isErc20: boolean;
isMultiPhase: boolean;
}
@@ -193,26 +187,39 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
contract,
tokenId,
isColumn,
contractV5,
isErc20,
isMultiPhase,
}) => {
// assume 1155 if we have a tokenId
const isErc1155 = tokenId !== undefined;
// if neither 1155 or 20 then it's 721
const isErc721 = !isErc20 && !isErc1155;
const walletAddress = useActiveAccount()?.address;
const trackEvent = useTrack();
const [resetFlag, setResetFlag] = useState(false);
const isAdmin = useIsAdmin(contractV5);
const [openSnapshotIndex, setOpenSnapshotIndex] = useState(-1);
const setClaimConditionsQuery = useSetClaimConditions(contract, tokenId);
const tokenDecimals = useReadContract(decimals, { contract: contractV5 });
const isAdmin = useIsAdmin(contract);
const [openSnapshotIndex, setOpenSnapshotIndex] = useState(-1);
const sendTx = useSendAndConfirmTransaction();
const tokenDecimals = useReadContract(decimals, {
contract,
queryOptions: {
enabled: isErc20,
},
});
const tokenDecimalsData = tokenDecimals.data ?? 0;
const saveClaimPhaseNotification = useTxNotifications(
"Saved claim phases",
"Failed to save claim phases",
);
const claimConditionsQuery = useClaimConditions(contract, tokenId, {
withAllowList: true,
const claimConditionsQuery = useReadContract(getClaimPhasesInLegacyFormat, {
contract,
...(isErc20
? { type: "erc20" }
: isErc721
? { type: "erc721" }
: { type: "erc1155", tokenId: BigInt(tokenId || 0) }),
});
const transformedQueryData = useMemo(() => {
@@ -227,7 +234,6 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
},
currencyAddress: phase.currencyAddress?.toLowerCase() || "0",
maxClaimablePerWallet: phase.maxClaimablePerWallet?.toString() || "0",
waitInSeconds: phase.waitInSeconds?.toString() || "0",
startTime: new Date(phase.startTime),
snapshot: phase.snapshot?.map(
({ address, maxClaimable, price, currencyAddress }) => ({
@@ -249,8 +255,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
);
}, [claimConditionsQuery.data, isMultiPhase]);
const isFetchingData =
claimConditionsQuery.isFetching || setClaimConditionsQuery.isLoading;
const isFetchingData = claimConditionsQuery.isFetching || sendTx.isPending;
const canEditForm = isAdmin && !isFetchingData;
@@ -346,10 +351,18 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
});
try {
await setClaimConditionsQuery.mutateAsync({
phases: d.phases as ClaimConditionInput[],
reset: resetFlag,
});
const tx = setClaimPhasesTx(
{
contract,
...(isErc20
? { type: "erc20" }
: isErc721
? { type: "erc721" }
: { type: "erc1155", tokenId: BigInt(tokenId || 0) }),
},
d.phases,
);
await sendTx.mutateAsync(tx);
trackEvent({
category,
action: "set-claim-conditions",
@@ -365,6 +378,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
form.setValue("phases", newPhases);
} catch (error) {
console.error(error);
trackEvent({
category,
action: "set-claim-conditions",
@@ -448,8 +462,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
)}
{controlledFields.map((field, index) => {
const dropType: DropType = field.snapshot
? isClaimPhaseV1 ||
field.maxClaimablePerWallet?.toString() === "0"
? field.maxClaimablePerWallet?.toString() === "0"
? "specific"
: "overrides"
: "any";
@@ -478,7 +491,6 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
<Fragment key={`snapshot_${field.id}_${index}`}>
<SnapshotUpload
dropType={dropType}
isV1ClaimCondition={isClaimPhaseV1}
isOpen={openSnapshotIndex === index}
onClose={() => setOpenSnapshotIndex(-1)}
value={snapshotValue}
@@ -496,7 +508,6 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
formDisabled: !canEditForm,
isErc20,
tokenDecimals: tokenDecimalsData,
isClaimPhaseV1,
dropType,
setOpenSnapshotIndex,
isAdmin,
@@ -507,14 +518,11 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
}}
>
<ClaimConditionsPhase
contract={contractV5}
contract={contract}
onRemove={() => {
removePhase(index);
if (!isMultiPhase) {
setResetFlag(true);
}
}}
isLoading={setClaimConditionsQuery.isLoading}
isLoading={sendTx.isPending}
/>
</ClaimsConditionFormContext.Provider>
</Fragment>
@@ -545,115 +553,92 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
gap={2}
>
<Flex gap={2}>
{contractV5 && (
<AdminOnly contract={contractV5}>
{!isClaimPhaseV1 ? (
<>
<Menu>
<MenuButton
as={Button}
size="sm"
colorScheme="primary"
variant={phases?.length > 0 ? "outline" : "solid"}
borderRadius="md"
leftIcon={<Icon as={FiPlus} />}
isDisabled={
setClaimConditionsQuery.isLoading ||
(!isMultiPhase && phases?.length > 0)
}
>
Add {isMultiPhase ? "Phase" : "Claim Conditions"}
</MenuButton>
<MenuList
borderRadius="lg"
overflow="hidden"
zIndex="overlay"
>
{Object.keys(ClaimConditionTypeData).map((key) => {
const type = key as ClaimConditionType;
<AdminOnly contract={contract}>
<Menu>
<MenuButton
as={Button}
size="sm"
colorScheme="primary"
variant={phases?.length > 0 ? "outline" : "solid"}
borderRadius="md"
leftIcon={<Icon as={FiPlus} />}
isDisabled={
sendTx.isPending || (!isMultiPhase && phases?.length > 0)
}
>
Add {isMultiPhase ? "Phase" : "Claim Conditions"}
</MenuButton>
<MenuList
borderRadius="lg"
overflow="hidden"
zIndex="overlay"
>
{Object.keys(ClaimConditionTypeData).map((key) => {
const type = key as ClaimConditionType;
if (type === "custom") {
return null;
}
if (type === "custom") {
return null;
}
return (
<MenuItem
key={type}
onClick={() => {
addPhase(type);
// TODO: Automatically start editing the new phase after adding it
}}
>
<div className="flex items-center gap-1">
{ClaimConditionTypeData[type].name}
<TooltipBox
content={
<Text size="body.md">
{ClaimConditionTypeData[type].description}
</Text>
}
/>
</div>
</MenuItem>
);
})}
</MenuList>
</Menu>
</AdminOnly>
return (
<MenuItem
key={type}
onClick={() => {
addPhase(type);
// TODO: Automatically start editing the new phase after adding it
}}
>
<div className="flex items-center gap-1">
{ClaimConditionTypeData[type].name}
<TooltipBox
content={
<Text size="body.md">
{
ClaimConditionTypeData[type]
.description
}
</Text>
}
/>
</div>
</MenuItem>
);
})}
</MenuList>
</Menu>
</>
) : (
<Button
size="sm"
colorScheme="primary"
variant="solid"
borderRadius="md"
leftIcon={<Icon as={FiPlus} />}
onClick={() => addPhase("custom")}
isDisabled={setClaimConditionsQuery.isLoading}
>
Add {isMultiPhase ? "Phase" : "Claim Conditions"}
</Button>
)}
</AdminOnly>
)}
{controlledFields.some((field) => field.fromSdk) && (
<ResetClaimEligibility
isErc20={isErc20}
contract={contractV5}
contract={contract}
tokenId={tokenId}
/>
)}
</Flex>
<Flex>
{contractV5 && (
<AdminOnly contract={contractV5} fallback={<Box pb={5} />}>
<Flex justifyContent="center" alignItems="center" gap={3}>
{(hasRemovedPhases || hasAddedPhases) && (
<Text color="red.500" fontWeight="bold">
You have unsaved changes
</Text>
)}
{controlledFields.length > 0 ||
hasRemovedPhases ||
!isMultiPhase ? (
<TransactionButton
colorScheme="primary"
transactionCount={1}
isDisabled={claimConditionsQuery.isLoading}
type="submit"
isLoading={setClaimConditionsQuery.isLoading}
loadingText="Saving..."
size="md"
>
Save Phases
</TransactionButton>
) : null}
</Flex>
</AdminOnly>
)}
<AdminOnly contract={contract} fallback={<Box pb={5} />}>
<Flex justifyContent="center" alignItems="center" gap={3}>
{(hasRemovedPhases || hasAddedPhases) && (
<Text color="red.500" fontWeight="bold">
You have unsaved changes
</Text>
)}
{controlledFields.length > 0 ||
hasRemovedPhases ||
!isMultiPhase ? (
<TransactionButton
colorScheme="primary"
transactionCount={1}
isDisabled={claimConditionsQuery.isLoading}
type="submit"
isLoading={sendTx.isPending}
loadingText="Saving..."
size="md"
>
Save Phases
</TransactionButton>
) : null}
</Flex>
</AdminOnly>
</Flex>
</Flex>
</Flex>
@@ -30,7 +30,6 @@ export const ClaimConditionsPhase: React.FC<ClaimConditionsPhaseProps> = ({
form,
field,
isErc20,
isClaimPhaseV1,
isAdmin,
claimConditionType,
isActive,
@@ -82,26 +81,15 @@ export const ClaimConditionsPhase: React.FC<ClaimConditionsPhaseProps> = ({
<Flex flexDir="column" gap={2} mt={{ base: 4, md: 0 }}>
<Flex gap={3} alignItems="center">
<Heading>
{isClaimPhaseV1
? isMultiPhase
? `Claim Phase ${phaseIndex + 1}`
: "Claim Conditions"
: ClaimConditionTypeData[claimConditionType].name}
</Heading>
<Heading>{ClaimConditionTypeData[claimConditionType].name}</Heading>
{isActive && (
<Badge colorScheme="green" borderRadius="lg" p={1.5}>
Currently active
</Badge>
)}
</Flex>
{isClaimPhaseV1 ? (
""
) : (
<Text>
{ClaimConditionTypeData[claimConditionType].description}
</Text>
)}
<Text>{ClaimConditionTypeData[claimConditionType].description}</Text>
</Flex>
{!field.isEditing ? (
@@ -1,5 +1,4 @@
import { Flex, Stack } from "@chakra-ui/react";
import { useContract } from "@thirdweb-dev/react";
import type { ThirdwebContract } from "thirdweb";
import { Heading, Text } from "tw-components";
import { ClaimConditionsForm } from "./claim-conditions-form/index";
@@ -16,8 +15,6 @@ export const ClaimConditions: React.FC<ClaimConditionsProps> = ({
isColumn,
isERC20,
}) => {
const contractQuery = useContract(contract.address);
return (
<Stack spacing={8}>
<Flex p={0} position="relative">
@@ -39,17 +36,14 @@ export const ClaimConditions: React.FC<ClaimConditionsProps> = ({
</Flex>
{/* Set Claim Conditions */}
{contractQuery.contract && (
<ClaimConditionsForm
isErc20={isERC20}
contract={contractQuery.contract}
tokenId={tokenId}
isColumn={isColumn}
contractV5={contract}
// always multi phase!
isMultiPhase={true}
/>
)}
<ClaimConditionsForm
isErc20={isERC20}
contract={contract}
tokenId={tokenId}
isColumn={isColumn}
// always multi phase!
isMultiPhase={true}
/>
</Flex>
</Flex>
</Stack>
@@ -87,9 +87,6 @@ export const ResetClaimEligibility: React.FC<ResetClaimEligibilityProps> = ({
return (
<AdminOnly contract={contract} fallback={<div className="pb-5" />}>
<TransactionButton
colorScheme="secondary"
bg="bgBlack"
color="bgWhite"
transactionCount={1}
type="button"
isLoading={sendTxMutation.isPending}
@@ -97,7 +94,7 @@ export const ResetClaimEligibility: React.FC<ResetClaimEligibilityProps> = ({
loadingText="Resetting..."
size="sm"
>
Reset Eligibility{" "}
Reset Eligibility
<ToolTipLabel
label={
<>
@@ -27,7 +27,7 @@ import {
} from "@chakra-ui/react";
import { Logo } from "components/logo";
import Papa from "papaparse";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { type DropzoneOptions, useDropzone } from "react-dropzone";
import { BsFillCloudUploadFill } from "react-icons/bs";
import { FiDownload } from "react-icons/fi";
@@ -64,7 +64,6 @@ interface SnapshotUploadProps {
}[]
| null;
dropType: "specific" | "any" | "overrides";
isV1ClaimCondition: boolean;
isDisabled: boolean;
}
@@ -74,7 +73,6 @@ export const SnapshotUpload: React.FC<SnapshotUploadProps> = ({
onClose,
value,
dropType,
isV1ClaimCondition,
isDisabled,
}) => {
const [validSnapshot, setValidSnapshot] = useState<SnapshotAddressInput[]>(
@@ -224,11 +222,7 @@ export const SnapshotUpload: React.FC<SnapshotUploadProps> = ({
</Flex>
{validSnapshot.length > 0 ? (
<SnapshotTable
portalRef={paginationPortalRef}
data={snapshotData}
isV1ClaimCondition={isV1ClaimCondition}
/>
<SnapshotTable portalRef={paginationPortalRef} data={snapshotData} />
) : (
<Flex flexGrow={1} align="center" overflow="auto">
<Container maxW="container.page">
@@ -275,33 +269,7 @@ export const SnapshotUpload: React.FC<SnapshotUploadProps> = ({
<Flex gap={2} flexDir="column">
<Heading size="label.md">Requirements</Heading>
<UnorderedList spacing={1}>
{isV1ClaimCondition ? (
<>
<Text as={ListItem}>
Files <em>must</em> contain one .csv file with a list
of addresses.
<br />
<Link download color="blue.500" href="/snapshot.csv">
<Icon as={FiDownload} /> Example snapshot
</Link>
</Text>
<Text as={ListItem}>
You may optionally add a <Code>maxClaimable</Code>{" "}
column to specify how many NFTs can be claimed by that
specific address per transaction, if not specified,
the default value is the one you have set on your
claim phase.
<br />
<Link
download
color="blue.500"
href="/snapshot-with-maxclaimable.csv"
>
<Icon as={FiDownload} /> Example snapshot
</Link>
</Text>
</>
) : dropType === "specific" ? (
{dropType === "specific" ? (
<>
<Text as={ListItem}>
Files <em>must</em> contain one .csv file with a list
@@ -384,13 +352,10 @@ export const SnapshotUpload: React.FC<SnapshotUploadProps> = ({
Repeated addresses will be removed and only the first
found will be kept.
</Text>
{!isV1ClaimCondition && (
<Text as={ListItem}>
The limit you set is for the maximum amount of NFTs a
wallet can claim, not how many they can receive in
total.
</Text>
)}
<Text as={ListItem}>
The limit you set is for the maximum amount of NFTs a
wallet can claim, not how many they can receive in total.
</Text>
</UnorderedList>
</Flex>
</Flex>
@@ -459,81 +424,67 @@ export const SnapshotUpload: React.FC<SnapshotUploadProps> = ({
interface SnapshotTableProps {
data: SnapshotAddressInput[];
portalRef: React.RefObject<HTMLDivElement>;
isV1ClaimCondition: boolean;
}
const SnapshotTable: React.FC<SnapshotTableProps> = ({
data,
portalRef,
isV1ClaimCondition,
}) => {
const columns = useMemo(() => {
let cols = [
{
Header: "Address",
accessor: ({ address, isValid }) => {
if (isValid) {
return address;
}
return (
<Flex>
<Tooltip
label={
address.startsWith("0x")
? "Address is not valid"
: "Address couldn't be resolved"
}
>
<Stack direction="row" align="center">
<Icon as={IoAlertCircleOutline} color="red.500" boxSize={5} />
<Text fontWeight="bold" color="red.500" cursor="default">
{address}
</Text>
</Stack>
</Tooltip>
</Flex>
);
},
},
{
Header: "Max claimable",
accessor: ({ maxClaimable }) => {
return maxClaimable === "0" || !maxClaimable
? "Default"
: maxClaimable === "unlimited"
? "Unlimited"
: maxClaimable;
},
},
] as Column<SnapshotAddressInput>[];
if (!isV1ClaimCondition) {
cols = cols.concat([
{
Header: "Price",
accessor: ({ price }) => {
return price === "0"
? "Free"
: !price || price === "unlimited"
? "Default"
: price;
},
},
{
Header: "Currency Address",
accessor: ({ currencyAddress }) => {
return currencyAddress ===
"0x0000000000000000000000000000000000000000" || !currencyAddress
? "Default"
: currencyAddress;
},
},
]);
}
return cols;
}, [isV1ClaimCondition]);
const SnapshotTableColumns = [
{
Header: "Address",
accessor: ({ address, isValid }) => {
if (isValid) {
return address;
}
return (
<Flex>
<Tooltip
label={
address.startsWith("0x")
? "Address is not valid"
: "Address couldn't be resolved"
}
>
<Stack direction="row" align="center">
<Icon as={IoAlertCircleOutline} color="red.500" boxSize={5} />
<Text fontWeight="bold" color="red.500" cursor="default">
{address}
</Text>
</Stack>
</Tooltip>
</Flex>
);
},
},
{
Header: "Max claimable",
accessor: ({ maxClaimable }) => {
return maxClaimable === "0" || !maxClaimable
? "Default"
: maxClaimable === "unlimited"
? "Unlimited"
: maxClaimable;
},
},
{
Header: "Price",
accessor: ({ price }) => {
return price === "0"
? "Free"
: !price || price === "unlimited"
? "Default"
: price;
},
},
{
Header: "Currency Address",
accessor: ({ currencyAddress }) => {
return currencyAddress === "0x0000000000000000000000000000000000000000" ||
!currencyAddress
? "Default"
: currencyAddress;
},
},
] as Column<SnapshotAddressInput>[];
const SnapshotTable: React.FC<SnapshotTableProps> = ({ data, portalRef }) => {
const {
getTableProps,
getTableBodyProps,
@@ -555,7 +506,7 @@ const SnapshotTable: React.FC<SnapshotTableProps> = ({
state: { pageIndex, pageSize },
} = useTable(
{
columns,
columns: SnapshotTableColumns,
data,
initialState: {
pageSize: 50,
@@ -18,7 +18,6 @@ export const NFTRevealButton: React.FC<NFTRevealButtonProps> = ({
const batchesQuery = useReadContract(getBatchesToReveal, {
contract,
});
console.log("*** batches", batchesQuery);
return batchesQuery.data?.length ? (
<MinterOnly contract={contract}>
<Drawer
+10
View File
@@ -54,6 +54,16 @@ function parseError(error: unknown): string | JSX.Element {
return error.message;
}
// handle rpc errors
if (
error &&
typeof error === "object" &&
"message" in error &&
typeof error.message === "string"
) {
return error.message;
}
// everything that falls through here should be logged and sent to posthog
console.error("unknown error", error);
posthog.capture("unknown_error", { error });
@@ -22,24 +22,30 @@ export async function convertErc20Amount(
} & AmountOrAmountInWei
>,
): Promise<bigint> {
if ("amount" in options) {
// for native token, we know decimals are 18
if (!options.erc20Address || isNativeTokenAddress(options.erc20Address)) {
return toUnits(options.amount.toString(), 18);
try {
if ("amount" in options) {
// for native token, we know decimals are 18
if (!options.erc20Address || isNativeTokenAddress(options.erc20Address)) {
return toUnits(options.amount.toString(), 18);
}
// otherwise get the decimals of the currency
const currencyContract = getContract({
client: options.client,
chain: options.chain,
address: options.erc20Address,
});
const { decimals } = await import(
"../../extensions/erc20/read/decimals.js"
);
const currencyDecimals = await decimals({
contract: currencyContract,
});
return toUnits(options.amount.toString(), currencyDecimals);
}
// otherwise get the decimals of the currency
const currencyContract = getContract({
client: options.client,
chain: options.chain,
address: options.erc20Address,
});
const { decimals } = await import(
"../../extensions/erc20/read/decimals.js"
return options.amountInWei;
} catch (e) {
throw new Error(
`Failed to convert ERC20 amount for token: ${options.erc20Address}: ${e}`,
);
const currencyDecimals = await decimals({
contract: currencyContract,
});
return toUnits(options.amount.toString(), currencyDecimals);
}
return options.amountInWei;
}