From 0f0e68a285ad725928ec785dbcf32a0a69697a62 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Tue, 9 Aug 2022 14:42:55 +0300 Subject: [PATCH] Fixes and improvements (#28) * Update collection creation error messages * Update minimum unit price * Update .env.example * Fix double ustars conversion for whitelist & minter unit_price * Add minimum unit price for whitelisted addresses * Fix: Invalid baseTokenURI error during collection instantiation * Collection cover image URI update * Minimum unit price update - 2 * Fix: nonfunctional existing whitelist option * Check matching asset and metadata file arrays before creating a collection * Mark minting detail inputs as required * Fix: collection creation with the specified royalty preference * Fix: whitelistType change problem * Fix creation logic * Automate number of tokens input & check per address limit * Automate number of tokens input & check per address limit - 2 * Metadata files should have .json extensions * Check royalty percentage * Upload service related changes now trigger state updates Co-authored-by: findolor --- .env.example | 7 +-- .../collections/creation/MintingDetails.tsx | 12 +++-- .../collections/creation/RoyaltyDetails.tsx | 4 +- .../collections/creation/UploadDetails.tsx | 26 ++++------ .../collections/creation/WhitelistDetails.tsx | 18 ++++++- pages/collections/create.tsx | 48 +++++++++++-------- utils/compareFileArrays.ts | 9 ++++ 7 files changed, 77 insertions(+), 47 deletions(-) create mode 100644 utils/compareFileArrays.ts diff --git a/.env.example b/.env.example index 71104f0..2097d1b 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,9 @@ APP_VERSION=0.1.0 -NEXT_PUBLIC_SG721_CODE_ID=5 - NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS +NEXT_PUBLIC_WHITELIST_CODE_ID=3 +NEXT_PUBLIC_MINTER_CODE_ID=2 +NEXT_PUBLIC_SG721_CODE_ID=1 NEXT_PUBLIC_API_URL=https:// NEXT_PUBLIC_BLOCK_EXPLORER_URL=https://testnet-explorer.publicawesome.dev/stargaze @@ -13,4 +14,4 @@ NEXT_PUBLIC_S3_BUCKET= # TODO NEXT_PUBLIC_S3_ENDPOINT= # TODO NEXT_PUBLIC_S3_KEY= # TODO NEXT_PUBLIC_S3_REGION= # TODO -NEXT_PUBLIC_S3_SECRET= # TODO +NEXT_PUBLIC_S3_SECRET= # TODO \ No newline at end of file diff --git a/components/collections/creation/MintingDetails.tsx b/components/collections/creation/MintingDetails.tsx index 6d0c12b..328e093 100644 --- a/components/collections/creation/MintingDetails.tsx +++ b/components/collections/creation/MintingDetails.tsx @@ -8,6 +8,7 @@ import { NumberInput } from '../../forms/FormInput' interface MintingDetailsProps { onChange: (data: MintingDetailsDataProps) => void + numberOfTokens: number | undefined } export interface MintingDetailsDataProps { @@ -17,7 +18,7 @@ export interface MintingDetailsDataProps { startTime: string } -export const MintingDetails = ({ onChange }: MintingDetailsProps) => { +export const MintingDetails = ({ onChange, numberOfTokens }: MintingDetailsProps) => { const [timestamp, setTimestamp] = useState() const numberOfTokensState = useNumberInputState({ @@ -45,6 +46,7 @@ export const MintingDetails = ({ onChange }: MintingDetailsProps) => { }) useEffect(() => { + if (numberOfTokens) numberOfTokensState.onChange(numberOfTokens) const data: MintingDetailsDataProps = { numTokens: numberOfTokensState.value, unitPrice: unitPriceState.value ? (Number(unitPriceState.value) * 1_000_000).toString() : '', @@ -53,14 +55,14 @@ export const MintingDetails = ({ onChange }: MintingDetailsProps) => { } onChange(data) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [numberOfTokensState.value, unitPriceState.value, perAddressLimitState.value, timestamp]) + }, [numberOfTokens, numberOfTokensState.value, unitPriceState.value, perAddressLimitState.value, timestamp]) return (
- - - + + + setTimestamp(date)} value={timestamp} /> diff --git a/components/collections/creation/RoyaltyDetails.tsx b/components/collections/creation/RoyaltyDetails.tsx index 4ef4454..a71184b 100644 --- a/components/collections/creation/RoyaltyDetails.tsx +++ b/components/collections/creation/RoyaltyDetails.tsx @@ -44,7 +44,7 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => { } onChange(data) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [royaltyPaymentAddressState.value, royaltyShareState.value]) + }, [royaltyState, royaltyPaymentAddressState.value, royaltyShareState.value]) return (
@@ -78,7 +78,7 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => { value="Existing" />
diff --git a/components/collections/creation/UploadDetails.tsx b/components/collections/creation/UploadDetails.tsx index aeb8a96..d7a89ed 100644 --- a/components/collections/creation/UploadDetails.tsx +++ b/components/collections/creation/UploadDetails.tsx @@ -98,11 +98,9 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => { if (event.target.files === null) return toast.error('No files selected.') for (let i = 0; i < event.target.files.length; i++) { reader = new FileReader() - reader.onload = async (e) => { + reader.onload = (e) => { if (!e.target?.result) return toast.error('Error parsing file.') if (!event.target.files) return toast.error('No files selected.') - if (!JSON.parse(await event.target.files[i].text()).attributes) - return toast.error(`The file with name '${event.target.files[i].name}' doesn't have an attributes list!`) const metadataFile = new File([e.target.result], event.target.files[i].name, { type: 'application/json' }) setMetadataFilesArray((prev) => [...prev, metadataFile]) } @@ -126,16 +124,6 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => { console.log(JSON.parse(await metadataFilesArray[metadataFileArrayIndex]?.text())) } - const checkAssetMetadataMatch = () => { - const metadataFileNames = metadataFilesArray.map((file) => file.name) - const assetFileNames = assetFilesArray.map((file) => file.name.substring(0, file.name.lastIndexOf('.'))) - // Compare the two arrays to make sure they are the same - const areArraysEqual = metadataFileNames.every((val, index) => val === assetFileNames[index]) - if (!areArraysEqual) { - throw new Error('Asset and metadata file names do not match.') - } - } - const videoPreviewElements = useMemo(() => { const tempArray: JSX.Element[] = [] assetFilesArray.forEach((assetFile) => { @@ -163,7 +151,6 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => { useEffect(() => { try { - checkAssetMetadataMatch() const data: UploadDetailsDataProps = { assetFiles: assetFilesArray, metadataFiles: metadataFilesArray, @@ -176,7 +163,14 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => { } catch (error: any) { toast.error(error.message) } - }, [assetFilesArray, metadataFilesArray]) + }, [ + assetFilesArray, + metadataFilesArray, + uploadService, + nftStorageApiKeyState.value, + pinataApiKeyState.value, + pinataSecretKeyState.value, + ]) useEffect(() => { setAssetFilesArray([]) @@ -383,7 +377,7 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => { )} > { id: 'whitelist-address', name: 'whitelistAddress', title: 'Whitelist Address', + defaultValue: '', }) const uniPriceState = useNumberInputState({ @@ -79,7 +80,16 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => { } onChange(data) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [uniPriceState.value, memberLimitState.value, perAddressLimitState.value, startDate, endDate, whitelistArray]) + }, [ + whitelistAddressState.value, + uniPriceState.value, + memberLimitState.value, + perAddressLimitState.value, + startDate, + endDate, + whitelistArray, + whitelistState, + ]) return (
@@ -135,7 +145,11 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
- + whitelistAddressState.onChange(e.target.value)} + /> diff --git a/pages/collections/create.tsx b/pages/collections/create.tsx index 7fa2742..46d4108 100644 --- a/pages/collections/create.tsx +++ b/pages/collections/create.tsx @@ -26,6 +26,7 @@ import useCollapse from 'react-collapsed' import { toast } from 'react-hot-toast' import type { UploadServiceType } from 'services/upload' import { upload } from 'services/upload' +import { compareFileArrays } from 'utils/compareFileArrays' import { MINTER_CODE_ID, SG721_CODE_ID, WHITELIST_CODE_ID } from 'utils/constants' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' @@ -58,6 +59,7 @@ const CollectionCreationPage: NextPage = () => { checkRoyaltyDetails() const baseUri = await uploadFiles() + //upload coverImageUri and append the file name const coverImageUri = await upload( collectionDetails?.imageFile as File[], uploadDetails?.uploadService as UploadServiceType, @@ -71,7 +73,7 @@ const CollectionCreationPage: NextPage = () => { if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() - await instantate(baseUri, coverImageUri, whitelist) + await instantiate(baseUri, coverImageUri, whitelist) // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { @@ -87,7 +89,7 @@ const CollectionCreationPage: NextPage = () => { members: whitelistDetails?.members, start_time: whitelistDetails?.startTime, end_time: whitelistDetails?.endTime, - unit_price: coin(String(Number(whitelistDetails?.unitPrice) * 1000000), 'ustars'), + unit_price: coin(String(Number(whitelistDetails?.unitPrice)), 'ustars'), per_address_limit: whitelistDetails?.perAddressLimit, member_limit: whitelistDetails?.memberLimit, } @@ -102,20 +104,20 @@ const CollectionCreationPage: NextPage = () => { return data.contractAddress } - const instantate = async (baseUri: string, coverImageUri: string, whitelist?: string) => { + const instantiate = async (baseUri: string, coverImageUri: string, whitelist?: string) => { if (!wallet.initialized) throw new Error('Wallet not connected') if (!minterContract) throw new Error('Contract not found') let royaltyInfo = null if (royaltyDetails?.royaltyType === 'new') { royaltyInfo = { - paymentAddress: royaltyDetails.paymentAddress, - share: royaltyDetails.share, + payment_address: royaltyDetails.paymentAddress, + share: (Number(royaltyDetails.share) / 100).toString(), } } const msg = { - base_token_uri: baseUri, + base_token_uri: `ipfs://${baseUri}/`, num_tokens: mintingDetails?.numTokens, sg721_code_id: SG721_CODE_ID, sg721_instantiate_msg: { @@ -125,19 +127,18 @@ const CollectionCreationPage: NextPage = () => { collection_info: { creator: wallet.address, description: collectionDetails?.description, - image: coverImageUri, - external_link: collectionDetails?.externalLink, + image: `ipfs://${coverImageUri}/${collectionDetails?.imageFile[0].name as string}`, + external_link: collectionDetails?.externalLink === '' ? null : collectionDetails?.externalLink, royalty_info: royaltyInfo, }, }, per_address_limit: mintingDetails?.perAddressLimit, - unit_price: coin(String(Number(mintingDetails?.unitPrice) * 1000000), 'ustars'), - whitelist_address: whitelist, + unit_price: coin(String(Number(mintingDetails?.unitPrice)), 'ustars'), + whitelist, start_time: mintingDetails?.startTime, } const data = await minterContract.instantiate(MINTER_CODE_ID, msg, 'Stargaze Minter Contract', wallet.address) - setTransactionHash(data.transactionHash) setContractAddress(data.contractAddress) } @@ -206,6 +207,7 @@ const CollectionCreationPage: NextPage = () => { if (uploadDetails.metadataFiles.length === 0) { throw new Error('Please upload metadatas') } + compareFileArrays(uploadDetails.assetFiles, uploadDetails.metadataFiles) if (uploadDetails.uploadService === 'nft-storage') { if (uploadDetails.nftStorageApiKey === '') { throw new Error('Please enter NFT Storage api key') @@ -217,17 +219,22 @@ const CollectionCreationPage: NextPage = () => { const checkCollectionDetails = () => { if (!collectionDetails) throw new Error('Please fill out the collection details') - if (collectionDetails.name === '') throw new Error('Name is required') - if (collectionDetails.description === '') throw new Error('Description is required') - if (collectionDetails.imageFile.length === 0) throw new Error('Cover image is required') + if (collectionDetails.name === '') throw new Error('Collection name is required') + if (collectionDetails.description === '') throw new Error('Collection description is required') + if (collectionDetails.imageFile.length === 0) throw new Error('Collection cover image is required') } const checkMintingDetails = () => { if (!mintingDetails) throw new Error('Please fill out the minting details') if (mintingDetails.numTokens < 1 || mintingDetails.numTokens > 10000) throw new Error('Invalid number of tokens') - if (Number(mintingDetails.unitPrice) < 500) throw new Error('Invalid unit price') - if (mintingDetails.perAddressLimit < 1 || mintingDetails.perAddressLimit > 50) - throw new Error('Per address limit is required') + if (Number(mintingDetails.unitPrice) < 50000000) + throw new Error('Invalid unit price: The minimum unit price is 50 STARS') + if ( + mintingDetails.perAddressLimit < 1 || + mintingDetails.perAddressLimit > 50 || + mintingDetails.perAddressLimit > mintingDetails.numTokens + ) + throw new Error('Invalid limit for tokens per address') if (mintingDetails.startTime === '') throw new Error('Start time is required') } @@ -235,9 +242,11 @@ const CollectionCreationPage: NextPage = () => { if (!whitelistDetails) throw new Error('Please fill out the whitelist details') if (whitelistDetails.whitelistType === 'existing') { if (whitelistDetails.contractAddress === '') throw new Error('Whitelist contract address is required') - } else { + } else if (whitelistDetails.whitelistType === 'new') { if (whitelistDetails.members?.length === 0) throw new Error('Whitelist member list cannot be empty') if (whitelistDetails.unitPrice === '') throw new Error('Whitelist unit price is required') + if (Number(whitelistDetails.unitPrice) < 25000000) + throw new Error('Invalid unit price: The minimum unit price for whitelisted addresses is 25 STARS') if (whitelistDetails.startTime === '') throw new Error('Start time is required') if (whitelistDetails.endTime === '') throw new Error('End time is required') if (whitelistDetails.perAddressLimit === 0) throw new Error('Per address limit is required') @@ -249,6 +258,7 @@ const CollectionCreationPage: NextPage = () => { if (!royaltyDetails) throw new Error('Please fill out the royalty details') if (royaltyDetails.royaltyType === 'new') { if (royaltyDetails.share === 0) throw new Error('Royalty share is required') + if (royaltyDetails.share > 100 || royaltyDetails.share < 0) throw new Error('Invalid royalty share') if (royaltyDetails.paymentAddress === '') throw new Error('Royalty payment address is required') } } @@ -274,7 +284,7 @@ const CollectionCreationPage: NextPage = () => {
- +
diff --git a/utils/compareFileArrays.ts b/utils/compareFileArrays.ts new file mode 100644 index 0000000..6e9f97c --- /dev/null +++ b/utils/compareFileArrays.ts @@ -0,0 +1,9 @@ +export const compareFileArrays = (assetFilesArray: File[], metadataFilesArray: File[]) => { + const metadataFileNames = metadataFilesArray.map((file) => file.name.substring(0, file.name.lastIndexOf('.'))) + const assetFileNames = assetFilesArray.map((file) => file.name.substring(0, file.name.lastIndexOf('.'))) + // Compare the two arrays to make sure they are the same + const areArraysEqual = metadataFileNames.every((val, index) => val === assetFileNames[index]) + if (!areArraysEqual) { + throw new Error('Asset and metadata file names do not match.') + } +}