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}`)
+ })
+ }
+}