diff --git a/.env.example b/.env.example index b9a4f58..fe5b903 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_VERSION=0.6.9 +APP_VERSION=0.7.0 NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS NEXT_PUBLIC_SG721_CODE_ID=2595 diff --git a/components/collections/creation/UploadDetails.tsx b/components/collections/creation/UploadDetails.tsx index e6b0f4c..a9b7a1d 100644 --- a/components/collections/creation/UploadDetails.tsx +++ b/components/collections/creation/UploadDetails.tsx @@ -13,6 +13,7 @@ import { useInputState } from 'components/forms/FormInput.hooks' import { MetadataInput } from 'components/MetadataInput' import { MetadataModal } from 'components/MetadataModal' import { SingleAssetPreview } from 'components/SingleAssetPreview' +import { Tooltip } from 'components/Tooltip' import { addLogItem } from 'contexts/log' import type { ChangeEvent } from 'react' import { useEffect, useRef, useState } from 'react' @@ -336,7 +337,14 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho and upload your assets & metadata manually to get a base URI for your collection.

- + + +
@@ -360,7 +368,14 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho and upload your asset & metadata manually to get a URI for your token before minting.

- + + +
- + + +
diff --git a/components/openEdition/OpenEditionMinterCreator.tsx b/components/openEdition/OpenEditionMinterCreator.tsx index b05295c..3f0fce3 100644 --- a/components/openEdition/OpenEditionMinterCreator.tsx +++ b/components/openEdition/OpenEditionMinterCreator.tsx @@ -28,6 +28,7 @@ import { } from 'utils/constants' import { getAssetType } from 'utils/getAssetType' import { isValidAddress } from 'utils/isValidAddress' +import { checkTokenUri } from 'utils/isValidTokenUri' import { uid } from 'utils/random' import { type CollectionDetailsDataProps, CollectionDetails } from './CollectionDetails' @@ -101,23 +102,30 @@ export const OpenEditionMinterCreator = ({ const performOpenEditionMinterChecks = () => { try { setReadyToCreate(false) - checkUploadDetails() checkCollectionDetails() checkMintingDetails() - void checkRoyaltyDetails() + void checkUploadDetails() .then(() => { - void checkwalletBalance() + void checkRoyaltyDetails() .then(() => { - setReadyToCreate(true) + void checkwalletBalance() + .then(() => { + setReadyToCreate(true) + }) + .catch((error: any) => { + toast.error(`Error in Wallet Balance: ${error.message}`, { style: { maxWidth: 'none' } }) + addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) + setReadyToCreate(false) + }) }) .catch((error: any) => { - toast.error(`Error in Wallet Balance: ${error.message}`, { style: { maxWidth: 'none' } }) + toast.error(`Error in Royalty Details: ${error.message}`, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setReadyToCreate(false) }) }) .catch((error: any) => { - toast.error(`Error in Royalty Details: ${error.message}`, { style: { maxWidth: 'none' } }) + toast.error(`Error in Upload Details: ${error.message}`, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setReadyToCreate(false) }) @@ -129,7 +137,7 @@ export const OpenEditionMinterCreator = ({ } } - const checkUploadDetails = () => { + const checkUploadDetails = async () => { if (!wallet.initialized) throw new Error('Wallet not connected.') if ( (metadataStorageMethod === 'off-chain' && !offChainMetadataUploadDetails) || @@ -200,6 +208,9 @@ export const OpenEditionMinterCreator = ({ throw new Error('Please enter a valid cover image URL') } } + if (offChainMetadataUploadDetails?.uploadMethod === 'existing') { + await checkTokenUri(offChainMetadataUploadDetails.tokenURI as string) + } } const checkCollectionDetails = () => { diff --git a/package.json b/package.json index 89b8e9a..f35e212 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stargaze-studio", - "version": "0.6.9", + "version": "0.7.0", "workspaces": [ "packages/*" ], diff --git a/pages/collections/create.tsx b/pages/collections/create.tsx index f13280e..c732ef3 100644 --- a/pages/collections/create.tsx +++ b/pages/collections/create.tsx @@ -63,6 +63,7 @@ import { WHITELIST_CODE_ID, WHITELIST_FLEX_CODE_ID, } from 'utils/constants' +import { checkTokenUri } from 'utils/isValidTokenUri' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' import { uid } from 'utils/random' @@ -140,26 +141,34 @@ const CollectionCreationPage: NextPage = () => { checkUploadDetails() checkCollectionDetails() checkMintingDetails() - void checkRoyaltyDetails() + void checkExistingTokenURI() .then(() => { - checkWhitelistDetails() + void checkRoyaltyDetails() .then(() => { - checkwalletBalance() - setReadyToCreateVm(true) + checkWhitelistDetails() + .then(() => { + checkwalletBalance() + setReadyToCreateVm(true) + }) + .catch((error) => { + if (String(error.message).includes('Insufficient wallet balance')) { + toast.error(`${error.message}`, { style: { maxWidth: 'none' } }) + addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) + } else { + toast.error(`Error in Whitelist Configuration: ${error.message}`, { style: { maxWidth: 'none' } }) + addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) + } + setReadyToCreateVm(false) + }) }) .catch((error) => { - if (String(error.message).includes('Insufficient wallet balance')) { - toast.error(`${error.message}`, { style: { maxWidth: 'none' } }) - addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) - } else { - toast.error(`Error in Whitelist Configuration: ${error.message}`, { style: { maxWidth: 'none' } }) - addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) - } + toast.error(`Error in Royalty Details: ${error.message}`, { style: { maxWidth: 'none' } }) + addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setReadyToCreateVm(false) }) }) .catch((error) => { - toast.error(`Error in Royalty Details: ${error.message}`, { style: { maxWidth: 'none' } }) + toast.error(`Error in Base Token URI: ${error.message}`, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setReadyToCreateVm(false) }) @@ -176,21 +185,29 @@ const CollectionCreationPage: NextPage = () => { setReadyToCreateBm(false) checkUploadDetails() checkCollectionDetails() - void checkRoyaltyDetails() + void checkExistingTokenURI() .then(() => { - checkWhitelistDetails() + void checkRoyaltyDetails() .then(() => { - checkwalletBalance() - setReadyToCreateBm(true) + checkWhitelistDetails() + .then(() => { + checkwalletBalance() + setReadyToCreateBm(true) + }) + .catch((error) => { + toast.error(`${error.message}`, { style: { maxWidth: 'none' } }) + addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) + setReadyToCreateBm(false) + }) }) .catch((error) => { - toast.error(`${error.message}`, { style: { maxWidth: 'none' } }) + toast.error(`Error in Royalty Configuration: ${error.message}`, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setReadyToCreateBm(false) }) }) .catch((error) => { - toast.error(`Error in Royalty Configuration: ${error.message}`, { style: { maxWidth: 'none' } }) + toast.error(`Error in Existing Token URI: ${error.message}`, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setReadyToCreateBm(false) }) @@ -205,12 +222,20 @@ const CollectionCreationPage: NextPage = () => { try { setReadyToUploadAndMint(false) checkUploadDetails() - checkWhitelistDetails() + checkExistingTokenURI() .then(() => { - setReadyToUploadAndMint(true) + checkWhitelistDetails() + .then(() => { + setReadyToUploadAndMint(true) + }) + .catch((error) => { + toast.error(`${error.message}`, { style: { maxWidth: 'none' } }) + addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) + setReadyToUploadAndMint(false) + }) }) .catch((error) => { - toast.error(`${error.message}`, { style: { maxWidth: 'none' } }) + toast.error(`Error in Token URI: ${error.message}`, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setReadyToUploadAndMint(false) }) @@ -818,6 +843,15 @@ const CollectionCreationPage: NextPage = () => { } } + const checkExistingTokenURI = async () => { + if (minterType === 'vending' && uploadDetails && uploadDetails.uploadMethod === 'existing') { + await checkTokenUri(uploadDetails.baseTokenURI as string, true) + } + if (minterType === 'base' && uploadDetails && uploadDetails.uploadMethod === 'existing') { + await checkTokenUri(uploadDetails.baseTokenURI as string) + } + } + const checkCollectionDetails = () => { if (!collectionDetails) throw new Error('Please fill out the collection details') if (collectionDetails.name === '') throw new Error('Collection name is required') diff --git a/utils/isValidTokenUri.ts b/utils/isValidTokenUri.ts new file mode 100644 index 0000000..ac857fa --- /dev/null +++ b/utils/isValidTokenUri.ts @@ -0,0 +1,71 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +export const checkTokenUri = async (tokenUri: string, isBaseTokenUri?: boolean) => { + if (isBaseTokenUri) { + await fetch(tokenUri.replace('ipfs://', 'https://ipfs.io/ipfs/').concat(tokenUri.endsWith('/') ? '1' : '/1')) + .then((res) => + res + .json() + .then((data) => { + if (!data.image) { + throw Error('Metadata validation failed. The metadata files must contain an image URL.') + } + if (!data.image.startsWith('ipfs://')) { + throw Error('Metadata file validation failed: The corresponding value for image must be an IPFS URL.') + } + }) + .catch(() => { + throw Error( + `Metadata validation failed. Please check that the metadata files in the IPFS folder are valid JSON.`, + ) + }), + ) + .catch(async () => { + await fetch( + tokenUri.replace('ipfs://', 'https://ipfs.io/ipfs/').concat(tokenUri.endsWith('/') ? '1.json' : '/1.json'), + ) + .then((response) => + response + .json() + .then((file) => { + if (!file.image) { + throw Error('Metadata validation failed. The metadata files must contain an image URL.') + } + if (!file.image.startsWith('ipfs://')) { + throw Error('Metadata file validation failed: The corresponding value for image must be an IPFS URL.') + } + }) + .catch(() => { + throw Error( + `Metadata validation failed. Please check that the metadata files in the IPFS folder are valid JSON.`, + ) + }), + ) + .catch(() => { + throw Error( + `Unable to fetch metadata from ${tokenUri}. Metadata validation failed. Please check that the base token URI points to an IPFS folder with metadata files in it.`, + ) + }) + }) + } else { + await fetch(tokenUri.replace('ipfs://', 'https://ipfs.io/ipfs/')) + .then((res) => + res + .json() + .then((file) => { + if (!file.image) { + throw Error('Token URI must contain an image URL.') + } + if (!file.image.startsWith('ipfs://')) { + throw Error('Metadata file: The corresponding value for image must be an IPFS URL.') + } + }) + .catch(() => { + throw Error(`Metadata file could not be parsed. Please check that it is valid JSON.`) + }), + ) + .catch(() => { + throw Error(`Unable to fetch metadata from ${tokenUri}`) + }) + } +}