/* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable no-nested-ternary */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { toUtf8 } from '@cosmjs/encoding' import { coin } from '@cosmjs/proto-signing' import { Sidetab } from '@typeform/embed-react' import clsx from 'clsx' import { Alert } from 'components/Alert' import { Anchor } from 'components/Anchor' import { AnchorButton } from 'components/AnchorButton' import { Button } from 'components/Button' import { CollectionDetails, MintingDetails, RoyaltyDetails, UploadDetails, WhitelistDetails, } from 'components/collections/creation' import type { BaseMinterDetailsDataProps } from 'components/collections/creation/BaseMinterDetails' import { BaseMinterDetails } from 'components/collections/creation/BaseMinterDetails' import type { CollectionDetailsDataProps } from 'components/collections/creation/CollectionDetails' import type { MintingDetailsDataProps } from 'components/collections/creation/MintingDetails' import type { RoyaltyDetailsDataProps } from 'components/collections/creation/RoyaltyDetails' import type { UploadDetailsDataProps } from 'components/collections/creation/UploadDetails' import type { WhitelistDetailsDataProps } from 'components/collections/creation/WhitelistDetails' import { Conditional } from 'components/Conditional' import { LoadingModal } from 'components/LoadingModal' import type { OpenEditionMinterCreatorDataProps } from 'components/openEdition/OpenEditionMinterCreator' import { OpenEditionMinterCreator } from 'components/openEdition/OpenEditionMinterCreator' import { useContracts } from 'contexts/contracts' import { addLogItem } from 'contexts/log' import { useWallet } from 'contexts/wallet' import type { DispatchExecuteArgs as BaseFactoryDispatchExecuteArgs } from 'contracts/baseFactory/messages/execute' import { dispatchExecute as baseFactoryDispatchExecute } from 'contracts/baseFactory/messages/execute' import type { DispatchExecuteArgs as VendingFactoryDispatchExecuteArgs } from 'contracts/vendingFactory/messages/execute' import { dispatchExecute as vendingFactoryDispatchExecute } from 'contracts/vendingFactory/messages/execute' import type { NextPage } from 'next' import { NextSeo } from 'next-seo' import { useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'react-hot-toast' import { upload } from 'services/upload' import { compareFileArrays } from 'utils/compareFileArrays' import { BASE_FACTORY_ADDRESS, BASE_FACTORY_UPDATABLE_ADDRESS, BLOCK_EXPLORER_URL, NETWORK, OPEN_EDITION_FACTORY_ADDRESS, OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS, SG721_CODE_ID, SG721_UPDATABLE_CODE_ID, STARGAZE_URL, VENDING_FACTORY_ADDRESS, VENDING_FACTORY_FLEX_ADDRESS, VENDING_FACTORY_UPDATABLE_ADDRESS, WHITELIST_CODE_ID, WHITELIST_FLEX_CODE_ID, } from 'utils/constants' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' import { uid } from 'utils/random' import type { MinterType } from '../../components/collections/actions/Combobox' import type { UploadMethod } from '../../components/collections/creation/UploadDetails' import { ConfirmationModal } from '../../components/ConfirmationModal' import { getAssetType } from '../../utils/getAssetType' import { isValidAddress } from '../../utils/isValidAddress' const CollectionCreationPage: NextPage = () => { const wallet = useWallet() const { baseMinter: baseMinterContract, vendingMinter: vendingMinterContract, whitelist: whitelistContract, vendingFactory: vendingFactoryContract, baseFactory: baseFactoryContract, } = useContracts() const scrollRef = useRef(null) const sidetabRef = useRef(null) const vendingFactoryMessages = useMemo( () => vendingFactoryContract?.use(VENDING_FACTORY_ADDRESS), [vendingFactoryContract, wallet.address], ) const baseFactoryMessages = useMemo( () => baseFactoryContract?.use(BASE_FACTORY_ADDRESS), [baseFactoryContract, wallet.address], ) const [uploadDetails, setUploadDetails] = useState(null) const [collectionDetails, setCollectionDetails] = useState(null) const [baseMinterDetails, setBaseMinterDetails] = useState(null) const [openEditionMinterDetails, setOpenEditionMinterDetails] = useState( null, ) const [mintingDetails, setMintingDetails] = useState(null) const [whitelistDetails, setWhitelistDetails] = useState(null) const [royaltyDetails, setRoyaltyDetails] = useState(null) const [minterType, setMinterType] = useState('vending') const [vendingMinterCreationFee, setVendingMinterCreationFee] = useState(null) const [baseMinterCreationFee, setBaseMinterCreationFee] = useState(null) const [vendingMinterUpdatableCreationFee, setVendingMinterUpdatableCreationFee] = useState(null) const [openEditionMinterCreationFee, setOpenEditionMinterCreationFee] = useState(null) const [openEditionMinterUpdatableCreationFee, setOpenEditionMinterUpdatableCreationFee] = useState( null, ) const [vendingMinterFlexCreationFee, setVendingMinterFlexCreationFee] = useState(null) const [baseMinterUpdatableCreationFee, setBaseMinterUpdatableCreationFee] = useState(null) const [minimumMintPrice, setMinimumMintPrice] = useState('0') const [minimumUpdatableMintPrice, setMinimumUpdatableMintPrice] = useState('0') const [minimumOpenEditionMintPrice, setMinimumOpenEditionMintPrice] = useState('0') const [minimumOpenEditionUpdatableMintPrice, setMinimumOpenEditionUpdatableMintPrice] = useState('0') const [minimumFlexMintPrice, setMinimumFlexMintPrice] = useState('0') const [uploading, setUploading] = useState(false) const [isMintingComplete, setIsMintingComplete] = useState(false) const [creatingCollection, setCreatingCollection] = useState(false) const [readyToCreateVm, setReadyToCreateVm] = useState(false) const [readyToCreateBm, setReadyToCreateBm] = useState(false) const [readyToUploadAndMint, setReadyToUploadAndMint] = useState(false) const [vendingMinterContractAddress, setVendingMinterContractAddress] = useState(null) const [sg721ContractAddress, setSg721ContractAddress] = useState(null) const [whitelistContractAddress, setWhitelistContractAddress] = useState(null) const [baseTokenUri, setBaseTokenUri] = useState(null) const [coverImageUrl, setCoverImageUrl] = useState(null) const [transactionHash, setTransactionHash] = useState(null) const performVendingMinterChecks = () => { try { setReadyToCreateVm(false) checkUploadDetails() checkCollectionDetails() checkMintingDetails() void checkRoyaltyDetails() .then(() => { 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) => { 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: any) { toast.error(error.message, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setUploading(false) setReadyToCreateVm(false) } } const performBaseMinterChecks = () => { try { setReadyToCreateBm(false) checkUploadDetails() checkCollectionDetails() void checkRoyaltyDetails() .then(() => { 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 in Royalty Configuration: ${error.message}`, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setReadyToCreateBm(false) }) } catch (error: any) { toast.error(error.message, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setUploading(false) } } const performUploadAndMintChecks = () => { try { setReadyToUploadAndMint(false) checkUploadDetails() 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: any) { toast.error(error.message, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setUploading(false) } } const resetReadyFlags = () => { setReadyToCreateVm(false) setReadyToCreateBm(false) setReadyToUploadAndMint(false) } const createVendingMinterCollection = async () => { try { setCreatingCollection(true) setBaseTokenUri(null) setCoverImageUrl(null) setVendingMinterContractAddress(null) setSg721ContractAddress(null) setWhitelistContractAddress(null) setTransactionHash(null) if (uploadDetails?.uploadMethod === 'new') { setUploading(true) const baseUri = await uploadFiles() //upload coverImageUri and append the file name const coverImageUri = await upload( collectionDetails?.imageFile as File[], uploadDetails.uploadService, 'cover', uploadDetails.nftStorageApiKey as string, uploadDetails.pinataApiKey as string, uploadDetails.pinataSecretKey as string, ) setUploading(false) setBaseTokenUri(baseUri) setCoverImageUrl(coverImageUri) let whitelist: string | undefined if (whitelistDetails?.whitelistState === 'existing') whitelist = whitelistDetails.contractAddress else if (whitelistDetails?.whitelistState === 'new') whitelist = await instantiateWhitelist() setWhitelistContractAddress(whitelist as string) await instantiateVendingMinter(baseUri, coverImageUri, whitelist) } else { setBaseTokenUri(uploadDetails?.baseTokenURI as string) setCoverImageUrl(uploadDetails?.imageUrl as string) let whitelist: string | undefined if (whitelistDetails?.whitelistState === 'existing') whitelist = whitelistDetails.contractAddress else if (whitelistDetails?.whitelistState === 'new') whitelist = await instantiateWhitelist() setWhitelistContractAddress(whitelist as string) await instantiateVendingMinter(baseTokenUri as string, coverImageUrl as string, whitelist) } setCreatingCollection(false) // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { toast.error(error.message, { style: { maxWidth: 'none' }, duration: 10000 }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setCreatingCollection(false) setUploading(false) } } const createBaseMinterCollection = async () => { try { setCreatingCollection(true) setBaseTokenUri(null) setCoverImageUrl(null) setVendingMinterContractAddress(null) setIsMintingComplete(false) setSg721ContractAddress(null) setWhitelistContractAddress(null) setTransactionHash(null) if (uploadDetails?.uploadMethod === 'new') { setUploading(true) const baseUri = await uploadFiles() //upload coverImageUri and append the file name const coverImageUri = await upload( collectionDetails?.imageFile as File[], uploadDetails.uploadService, 'cover', uploadDetails.nftStorageApiKey as string, uploadDetails.pinataApiKey as string, uploadDetails.pinataSecretKey as string, ) setUploading(false) if (uploadDetails.assetFiles.length === 1) { setBaseTokenUri( `${baseUri}/${(uploadDetails.baseMinterMetadataFile as File).name.substring( 0, (uploadDetails.baseMinterMetadataFile as File).name.lastIndexOf('.'), )}`, ) } else { setBaseTokenUri(baseUri) } setCoverImageUrl(coverImageUri) if (uploadDetails.assetFiles.length === 1) { await instantiateBaseMinter( `ipfs://${baseUri}/${(uploadDetails.baseMinterMetadataFile as File).name.substring( 0, (uploadDetails.baseMinterMetadataFile as File).name.lastIndexOf('.'), )}`, coverImageUri, ) } else { await instantiateBaseMinter(`ipfs://${baseUri}`, coverImageUri) } } else { setBaseTokenUri(uploadDetails?.baseTokenURI as string) setCoverImageUrl(uploadDetails?.imageUrl as string) await instantiateBaseMinter(uploadDetails?.baseTokenURI as string, uploadDetails?.imageUrl as string) } setCreatingCollection(false) // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { toast.error(error.message, { style: { maxWidth: 'none' }, duration: 10000 }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setCreatingCollection(false) setUploading(false) } } const uploadAndMint = async () => { try { if (!wallet.initialized) throw new Error('Wallet not connected') if (!baseMinterContract) throw new Error('Contract not found') setCreatingCollection(true) setIsMintingComplete(false) setBaseTokenUri(null) setCoverImageUrl(null) setVendingMinterContractAddress(null) setSg721ContractAddress(null) setTransactionHash(null) if (uploadDetails?.uploadMethod === 'new') { console.log(JSON.stringify(uploadDetails.baseMinterMetadataFile?.text())) setUploading(true) await uploadFiles() .then(async (baseUri) => { setUploading(false) if (uploadDetails.assetFiles.length === 1) { setBaseTokenUri( `${baseUri}/${(uploadDetails.baseMinterMetadataFile as File).name.substring( 0, (uploadDetails.baseMinterMetadataFile as File).name.lastIndexOf('.'), )}`, ) const result = await baseMinterContract .use(baseMinterDetails?.existingBaseMinter as string) ?.mint( wallet.address, `ipfs://${baseUri}/${(uploadDetails.baseMinterMetadataFile as File).name.substring( 0, (uploadDetails.baseMinterMetadataFile as File).name.lastIndexOf('.'), )}`, ) console.log(result) return result } setBaseTokenUri(baseUri) const result = await baseMinterContract .use(baseMinterDetails?.existingBaseMinter as string) ?.batchMint(wallet.address, `ipfs://${baseUri}`, uploadDetails.assetFiles.length) console.log(result) return result }) .then((result) => { toast.success(`Token(s) minted & added to the collection successfully! Tx Hash: ${result}`, { style: { maxWidth: 'none' }, duration: 5000, }) setIsMintingComplete(true) setSg721ContractAddress(baseMinterDetails?.selectedCollectionAddress as string) setTransactionHash(result as string) }) .catch((error) => { toast.error(error.message, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setUploading(false) setCreatingCollection(false) setIsMintingComplete(false) }) } else { setBaseTokenUri(uploadDetails?.baseTokenURI as string) setUploading(false) await baseMinterContract .use(baseMinterDetails?.existingBaseMinter as string) ?.mint(wallet.address, `${uploadDetails?.baseTokenURI?.trim()}`) .then((result) => { toast.success(`Token minted & added to the collection successfully! Tx Hash: ${result}`, { style: { maxWidth: 'none' }, duration: 5000, }) setIsMintingComplete(true) setSg721ContractAddress(baseMinterDetails?.selectedCollectionAddress as string) setTransactionHash(result) }) .catch((error) => { toast.error(error.message, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setUploading(false) setCreatingCollection(false) }) } setCreatingCollection(false) // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { toast.error(error.message, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setCreatingCollection(false) setUploading(false) } } const instantiateWhitelist = async () => { if (!wallet.initialized) throw new Error('Wallet not connected') if (!whitelistContract) throw new Error('Contract not found') const standardMsg = { members: whitelistDetails?.members, start_time: whitelistDetails?.startTime, end_time: whitelistDetails?.endTime, mint_price: coin(String(Number(whitelistDetails?.unitPrice)), 'ustars'), per_address_limit: whitelistDetails?.perAddressLimit, member_limit: whitelistDetails?.memberLimit, admins: whitelistDetails?.admins || [wallet.address], admins_mutable: whitelistDetails?.adminsMutable, } const flexMsg = { members: whitelistDetails?.members, start_time: whitelistDetails?.startTime, end_time: whitelistDetails?.endTime, mint_price: coin(String(Number(whitelistDetails?.unitPrice)), 'ustars'), member_limit: whitelistDetails?.memberLimit, admins: whitelistDetails?.admins || [wallet.address], admins_mutable: whitelistDetails?.adminsMutable, } const data = await whitelistContract.instantiate( whitelistDetails?.whitelistType === 'standard' ? WHITELIST_CODE_ID : WHITELIST_FLEX_CODE_ID, whitelistDetails?.whitelistType === 'standard' ? standardMsg : flexMsg, 'Stargaze Whitelist Contract', wallet.address, ) return data.contractAddress } const instantiateVendingMinter = async (baseUri: string, coverImageUri: string, whitelist?: string) => { if (!wallet.initialized) throw new Error('Wallet not connected') if (!vendingFactoryContract) throw new Error('Contract not found') let royaltyInfo = null if (royaltyDetails?.royaltyType === 'new') { royaltyInfo = { payment_address: royaltyDetails.paymentAddress.trim(), share: (Number(royaltyDetails.share) / 100).toString(), } } const msg = { create_minter: { init_msg: { base_token_uri: `${uploadDetails?.uploadMethod === 'new' ? `ipfs://${baseUri}` : `${baseUri}`}`, start_time: mintingDetails?.startTime, num_tokens: mintingDetails?.numTokens, payment_address: mintingDetails?.paymentAddress ? mintingDetails.paymentAddress : undefined, mint_price: { amount: mintingDetails?.unitPrice, denom: 'ustars', }, per_address_limit: mintingDetails?.perAddressLimit, whitelist, }, collection_params: { code_id: collectionDetails?.updatable ? SG721_UPDATABLE_CODE_ID : SG721_CODE_ID, name: collectionDetails?.name, symbol: collectionDetails?.symbol, info: { creator: wallet.address, description: collectionDetails?.description, image: `${ uploadDetails?.uploadMethod === 'new' ? `ipfs://${coverImageUri}/${collectionDetails?.imageFile[0].name as string}` : `${coverImageUri}` }`, external_link: collectionDetails?.externalLink, explicit_content: collectionDetails?.explicit, royalty_info: royaltyInfo, start_trading_time: collectionDetails?.startTradingTime || null, }, }, }, } const payload: VendingFactoryDispatchExecuteArgs = { contract: whitelistDetails?.whitelistState !== 'none' && whitelistDetails?.whitelistType === 'flex' ? VENDING_FACTORY_FLEX_ADDRESS : collectionDetails?.updatable ? VENDING_FACTORY_UPDATABLE_ADDRESS : VENDING_FACTORY_ADDRESS, messages: vendingFactoryMessages, txSigner: wallet.address, msg, funds: [ coin( whitelistDetails?.whitelistState !== 'none' && whitelistDetails?.whitelistType === 'flex' ? (vendingMinterFlexCreationFee as string) : collectionDetails?.updatable ? (vendingMinterUpdatableCreationFee as string) : (vendingMinterCreationFee as string), 'ustars', ), ], updatable: collectionDetails?.updatable, flex: whitelistDetails?.whitelistState !== 'none' && whitelistDetails?.whitelistType === 'flex', } const data = await vendingFactoryDispatchExecute(payload) setTransactionHash(data.transactionHash) setVendingMinterContractAddress(data.vendingMinterAddress) setSg721ContractAddress(data.sg721Address) } const instantiateBaseMinter = async (baseUri: string, coverImageUri: string) => { if (!wallet.initialized) throw new Error('Wallet not connected') if (!baseFactoryContract) throw new Error('Contract not found') if (!baseMinterContract) throw new Error('Contract not found') let royaltyInfo = null if (royaltyDetails?.royaltyType === 'new') { royaltyInfo = { payment_address: royaltyDetails.paymentAddress.trim(), share: (Number(royaltyDetails.share) / 100).toString(), } } const msg = { create_minter: { init_msg: null, collection_params: { code_id: collectionDetails?.updatable ? SG721_UPDATABLE_CODE_ID : SG721_CODE_ID, name: collectionDetails?.name, symbol: collectionDetails?.symbol, info: { creator: wallet.address, description: collectionDetails?.description, image: `${ uploadDetails?.uploadMethod === 'new' ? `ipfs://${coverImageUri}/${collectionDetails?.imageFile[0].name as string}` : `${coverImageUri}` }`, external_link: collectionDetails?.externalLink, explicit_content: collectionDetails?.explicit, royalty_info: royaltyInfo, start_trading_time: null, //collectionDetails?.startTradingTime || null, }, }, }, } const payload: BaseFactoryDispatchExecuteArgs = { contract: collectionDetails?.updatable ? BASE_FACTORY_UPDATABLE_ADDRESS : BASE_FACTORY_ADDRESS, messages: baseFactoryMessages, txSigner: wallet.address, msg, funds: [ coin( collectionDetails?.updatable ? (baseMinterUpdatableCreationFee as string) : (baseMinterCreationFee as string), 'ustars', ), ], updatable: collectionDetails?.updatable, } await baseFactoryDispatchExecute(payload) .then(async (data) => { setTransactionHash(data.transactionHash) setVendingMinterContractAddress(data.baseMinterAddress) setSg721ContractAddress(data.sg721Address) if (uploadDetails?.assetFiles.length === 1 || uploadDetails?.uploadMethod === 'existing') { await toast .promise( baseMinterContract.use(data.baseMinterAddress)?.mint(wallet.address, baseUri) as Promise, { loading: 'Minting token...', success: (result) => { setIsMintingComplete(true) return `Token minted successfully! Tx Hash: ${result}` }, error: (error) => `Failed to mint token: ${error.message}`, }, { style: { maxWidth: 'none' } }, ) .catch((error) => { toast.error(error.message, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setUploading(false) setIsMintingComplete(false) setCreatingCollection(false) }) } else { console.log('Here') console.log(data.baseMinterAddress) await toast .promise( baseMinterContract .use(data.baseMinterAddress) ?.batchMint(wallet.address, baseUri, uploadDetails?.assetFiles.length as number) as Promise, { loading: 'Minting tokens...', success: (result) => { setIsMintingComplete(true) return `Tokens minted successfully! Tx Hash: ${result}` }, error: (error) => `Failed to mint tokens: ${error.message}`, }, { style: { maxWidth: 'none' } }, ) .catch((error) => { toast.error(error.message, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setUploading(false) setIsMintingComplete(false) setCreatingCollection(false) }) } setUploading(false) setCreatingCollection(false) }) .catch((error) => { toast.error(error.message, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setUploading(false) setCreatingCollection(false) }) } const uploadFiles = async (): Promise => { if (!uploadDetails) throw new Error('Please upload asset and metadata') return new Promise((resolve, reject) => { upload( uploadDetails.assetFiles, uploadDetails.uploadService, 'assets', uploadDetails.nftStorageApiKey as string, uploadDetails.pinataApiKey as string, uploadDetails.pinataSecretKey as string, ) .then((assetUri: string) => { if (minterType === 'vending' || (minterType === 'base' && uploadDetails.assetFiles.length > 1)) { const fileArray: File[] = [] let reader: FileReader for (let i = 0; i < uploadDetails.metadataFiles.length; i++) { reader = new FileReader() reader.onload = (e) => { const data: any = JSON.parse(e.target?.result as string) if ( getAssetType(uploadDetails.assetFiles[i].name) === 'audio' || getAssetType(uploadDetails.assetFiles[i].name) === 'video' || getAssetType(uploadDetails.assetFiles[i].name) === 'html' ) { data.animation_url = `ipfs://${assetUri}/${uploadDetails.assetFiles[i].name}` } if (getAssetType(uploadDetails.assetFiles[i].name) !== 'html') data.image = `ipfs://${assetUri}/${uploadDetails.assetFiles[i].name}` const metadataFileBlob = new Blob([JSON.stringify(data)], { type: 'application/json', }) const updatedMetadataFile = new File( [metadataFileBlob], uploadDetails.metadataFiles[i].name.substring( 0, uploadDetails.metadataFiles[i].name.lastIndexOf('.'), ), { type: 'application/json', }, ) fileArray.push(updatedMetadataFile) } reader.onloadend = () => { if (i === uploadDetails.metadataFiles.length - 1) { upload( fileArray, uploadDetails.uploadService, 'metadata', uploadDetails.nftStorageApiKey as string, uploadDetails.pinataApiKey as string, uploadDetails.pinataSecretKey as string, ) .then(resolve) .catch(reject) } } reader.readAsText(uploadDetails.metadataFiles[i], 'utf8') } } else if (minterType === 'base' && uploadDetails.assetFiles.length === 1) { const fileArray: File[] = [] const reader: FileReader = new FileReader() reader.onload = (e) => { const data: any = JSON.parse(e.target?.result as string) if ( getAssetType(uploadDetails.assetFiles[0].name) === 'audio' || getAssetType(uploadDetails.assetFiles[0].name) === 'video' || getAssetType(uploadDetails.assetFiles[0].name) === 'html' ) { data.animation_url = `ipfs://${assetUri}/${uploadDetails.assetFiles[0].name}` } if (getAssetType(uploadDetails.assetFiles[0].name) !== 'html') data.image = `ipfs://${assetUri}/${uploadDetails.assetFiles[0].name}` const metadataFileBlob = new Blob([JSON.stringify(data)], { type: 'application/json', }) console.log('Name: ', (uploadDetails.baseMinterMetadataFile as File).name) const updatedMetadataFile = new File( [metadataFileBlob], (uploadDetails.baseMinterMetadataFile as File).name.substring( 0, (uploadDetails.baseMinterMetadataFile as File).name.lastIndexOf('.'), ), { type: 'application/json', }, ) fileArray.push(updatedMetadataFile) } reader.onloadend = () => { upload( fileArray, uploadDetails.uploadService, 'metadata', uploadDetails.nftStorageApiKey as string, uploadDetails.pinataApiKey as string, uploadDetails.pinataSecretKey as string, ) .then(resolve) .catch(reject) } console.log('File: ', uploadDetails.baseMinterMetadataFile) reader.readAsText(uploadDetails.baseMinterMetadataFile as File, 'utf8') } }) .catch(reject) }) } const checkUploadDetails = () => { if (!wallet.initialized) throw new Error('Wallet not connected.') if (!uploadDetails) { throw new Error('Please select assets and metadata') } if ( minterType === 'base' && uploadDetails.uploadMethod === 'new' && uploadDetails.assetFiles.length > 1 && uploadDetails.metadataFiles.length === 0 ) { throw new Error('Please select metadata files') } if (uploadDetails.uploadMethod === 'new' && uploadDetails.assetFiles.length === 0) { throw new Error('Please select the assets') } if (minterType === 'vending' && uploadDetails.uploadMethod === 'new' && uploadDetails.metadataFiles.length === 0) { throw new Error('Please select the metadata files') } if (uploadDetails.uploadMethod === 'new' && minterType === 'vending') compareFileArrays(uploadDetails.assetFiles, uploadDetails.metadataFiles) if (uploadDetails.uploadMethod === 'new') { if (uploadDetails.uploadService === 'nft-storage') { if (uploadDetails.nftStorageApiKey === '') { throw new Error('Please enter a valid NFT.Storage API key') } } else if (uploadDetails.pinataApiKey === '' || uploadDetails.pinataSecretKey === '') { throw new Error('Please enter Pinata API and secret keys') } } if (uploadDetails.uploadMethod === 'existing' && !uploadDetails.baseTokenURI?.includes('ipfs://')) { throw new Error('Please specify a valid base token URI') } if (baseMinterDetails?.baseMinterAcquisitionMethod === 'existing' && !baseMinterDetails.existingBaseMinter) { throw new Error('Please specify a valid Base Minter contract address') } } const checkCollectionDetails = () => { if (!collectionDetails) throw new Error('Please fill out the collection details') if (collectionDetails.name === '') throw new Error('Collection name is required') if (collectionDetails.description === '') throw new Error('Collection description is required') if (collectionDetails.description.length > 512) throw new Error('Collection description cannot exceed 512 characters') if (uploadDetails?.uploadMethod === 'new' && collectionDetails.imageFile.length === 0) throw new Error('Collection cover image is required') if ( collectionDetails.startTradingTime && Number(collectionDetails.startTradingTime) < new Date().getTime() * 1000000 ) throw new Error('Invalid trading start time') if ( collectionDetails.startTradingTime && Number(collectionDetails.startTradingTime) < Number(mintingDetails?.startTime) ) throw new Error('Trading start time must be after minting start time') if (collectionDetails.externalLink) { try { const url = new URL(collectionDetails.externalLink) } catch (e: any) { throw new Error(`Invalid external link: Make sure to include the protocol (e.g. https://)`) } } } 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 (mintingDetails.unitPrice === '') throw new Error('Public mint price is required') if (whitelistDetails?.whitelistState !== 'none' && whitelistDetails?.whitelistType === 'flex') { if (Number(mintingDetails.unitPrice) < Number(minimumFlexMintPrice)) throw new Error(`Invalid unit price: The minimum unit price is ${Number(minimumFlexMintPrice) / 1000000} STARS`) } else if (collectionDetails?.updatable) { if (Number(mintingDetails.unitPrice) < Number(minimumUpdatableMintPrice)) throw new Error( `Invalid unit price: The minimum unit price is ${Number(minimumUpdatableMintPrice) / 1000000} STARS`, ) } else if (Number(mintingDetails.unitPrice) < Number(minimumMintPrice)) throw new Error(`Invalid unit price: The minimum unit price is ${Number(minimumMintPrice) / 1000000} STARS`) if ( !mintingDetails.perAddressLimit || mintingDetails.perAddressLimit < 1 || mintingDetails.perAddressLimit > 50 || mintingDetails.perAddressLimit > mintingDetails.numTokens ) throw new Error('Invalid limit for tokens per address') if (mintingDetails.numTokens < 100 && mintingDetails.perAddressLimit > 3) throw new Error( 'Invalid limit for tokens per address. Tokens per address limit cannot exceed 3 for collections with less than 100 tokens in total.', ) if ( mintingDetails.numTokens >= 100 && mintingDetails.perAddressLimit > Math.ceil((mintingDetails.numTokens / 100) * 3) ) throw new Error( 'Invalid limit for tokens per address. Tokens per address limit cannot exceed 3% of the total number of tokens in the collection.', ) if (mintingDetails.startTime === '') throw new Error('Start time is required') if (Number(mintingDetails.startTime) < new Date().getTime() * 1000000) throw new Error('Invalid start time') if ( mintingDetails.paymentAddress && (!isValidAddress(mintingDetails.paymentAddress) || !mintingDetails.paymentAddress.startsWith('stars1')) ) throw new Error('Invalid payment address') } const checkWhitelistDetails = async () => { if (!whitelistDetails) throw new Error('Please fill out the whitelist details') if (whitelistDetails.whitelistState === 'existing') { if (whitelistDetails.contractAddress === '') throw new Error('Whitelist contract address is required') else { const contract = whitelistContract?.use(whitelistDetails.contractAddress) //check if the address belongs to a whitelist contract (see performChecks()) const config = await contract?.config() if (JSON.stringify(config).includes('whale_cap')) whitelistDetails.whitelistType = 'flex' else whitelistDetails.whitelistType = 'standard' if (Number(config?.start_time) !== Number(mintingDetails?.startTime)) { const whitelistStartDate = new Date(Number(config?.start_time) / 1000000) throw Error(`Whitelist start time (${whitelistStartDate.toLocaleString()}) does not match minting start time`) } if (mintingDetails?.numTokens && config?.per_address_limit) { if (mintingDetails.numTokens >= 100 && Number(config.per_address_limit) > 50) { throw Error( `Invalid limit for tokens per address (${config.per_address_limit} tokens). Tokens per address limit cannot exceed 50 regardless of the total number of tokens.`, ) } else if ( mintingDetails.numTokens >= 100 && Number(config.per_address_limit) > Math.ceil((mintingDetails.numTokens / 100) * 3) ) { throw Error( `Invalid limit for tokens per address (${config.per_address_limit} tokens). Tokens per address limit cannot exceed 3% of the total number of tokens in the collection.`, ) } else if (mintingDetails.numTokens < 100 && Number(config.per_address_limit) > 3) { throw Error( `Invalid limit for tokens per address (${config.per_address_limit} tokens). Tokens per address limit cannot exceed 3 for collections with less than 100 tokens in total.`, ) } } } } else if (whitelistDetails.whitelistState === 'new') { if (whitelistDetails.members?.length === 0) throw new Error('Whitelist member list cannot be empty') if (whitelistDetails.unitPrice === undefined) throw new Error('Whitelist unit price is required') if (Number(whitelistDetails.unitPrice) < 0) throw new Error('Invalid unit price: The unit price cannot be negative') if (whitelistDetails.startTime === '') throw new Error('Start time is required') if (whitelistDetails.endTime === '') throw new Error('End time is required') if ( whitelistDetails.whitelistType === 'standard' && (!whitelistDetails.perAddressLimit || whitelistDetails.perAddressLimit === 0) ) throw new Error('Per address limit is required') if (!whitelistDetails.memberLimit || whitelistDetails.memberLimit === 0) throw new Error('Member limit is required') if (Number(whitelistDetails.startTime) >= Number(whitelistDetails.endTime)) throw new Error('Whitelist start time cannot be equal to or later than the whitelist end time') if (Number(whitelistDetails.startTime) !== Number(mintingDetails?.startTime)) throw new Error('Whitelist start time must be the same as the minting start time') if (whitelistDetails.perAddressLimit && mintingDetails?.numTokens) { if (mintingDetails.numTokens >= 100 && whitelistDetails.perAddressLimit > 50) { throw Error( `Invalid limit for tokens per address. Tokens per address limit cannot exceed 50 regardless of the total number of tokens.`, ) } else if ( mintingDetails.numTokens >= 100 && whitelistDetails.perAddressLimit > Math.ceil((mintingDetails.numTokens / 100) * 3) ) { throw Error( `Invalid limit for tokens per address. Tokens per address limit cannot exceed 3% of the total number of tokens in the collection.`, ) } else if (mintingDetails.numTokens < 100 && whitelistDetails.perAddressLimit > 3) { throw Error( `Invalid limit for tokens per address. Tokens per address limit cannot exceed 3 for collections with less than 100 tokens in total.`, ) } } } } const checkRoyaltyDetails = async () => { if (!royaltyDetails) throw new Error('Please fill out the royalty details') if (royaltyDetails.royaltyType === 'new') { if (royaltyDetails.share === 0) throw new Error('Royalty share percentage is required') if (royaltyDetails.share > 100 || royaltyDetails.share < 0) throw new Error('Invalid royalty share percentage') if (royaltyDetails.paymentAddress === '') throw new Error('Royalty payment address is required') if (!isValidAddress(royaltyDetails.paymentAddress.trim())) { if (royaltyDetails.paymentAddress.trim().endsWith('.stars')) { throw new Error('Royalty payment address could not be resolved') } throw new Error('Invalid royalty payment address') } const contractInfoResponse = await wallet.client ?.queryContractRaw( royaltyDetails.paymentAddress.trim(), toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()), ) .catch((e) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-call if (e.message.includes('bech32')) throw new Error('Invalid royalty payment address.') console.log(e.message) }) if (contractInfoResponse !== undefined) { const contractInfo = JSON.parse(new TextDecoder().decode(contractInfoResponse as Uint8Array)) // eslint-disable-next-line @typescript-eslint/no-unsafe-call if (contractInfo && !contractInfo.contract.includes('splits')) throw new Error('The provided royalty payment address does not belong to a splits contract.') else console.log(contractInfo) } } } const fetchFactoryParameters = async () => { const client = wallet.client if (!client) return if (BASE_FACTORY_ADDRESS) { const baseFactoryParameters = await client .queryContractSmart(BASE_FACTORY_ADDRESS, { params: {} }) .catch((error) => { toast.error(`${error.message}`, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) }) setBaseMinterCreationFee(baseFactoryParameters?.params?.creation_fee?.amount) } if (BASE_FACTORY_UPDATABLE_ADDRESS) { const baseFactoryUpdatableParameters = await client .queryContractSmart(BASE_FACTORY_UPDATABLE_ADDRESS, { params: {}, }) .catch((error) => { toast.error(`${error.message}`, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) }) setBaseMinterUpdatableCreationFee(baseFactoryUpdatableParameters?.params?.creation_fee?.amount) } if (VENDING_FACTORY_ADDRESS) { const vendingFactoryParameters = await client .queryContractSmart(VENDING_FACTORY_ADDRESS, { params: {} }) .catch((error) => { toast.error(`${error.message}`, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) }) setVendingMinterCreationFee(vendingFactoryParameters?.params?.creation_fee?.amount) setMinimumMintPrice(vendingFactoryParameters?.params?.min_mint_price?.amount) } if (VENDING_FACTORY_UPDATABLE_ADDRESS) { const vendingFactoryUpdatableParameters = await client .queryContractSmart(VENDING_FACTORY_UPDATABLE_ADDRESS, { params: {}, }) .catch((error) => { toast.error(`${error.message}`, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) }) setVendingMinterUpdatableCreationFee(vendingFactoryUpdatableParameters?.params?.creation_fee?.amount) setMinimumUpdatableMintPrice(vendingFactoryUpdatableParameters?.params?.min_mint_price?.amount) } if (VENDING_FACTORY_FLEX_ADDRESS) { const vendingFactoryFlexParameters = await client .queryContractSmart(VENDING_FACTORY_FLEX_ADDRESS, { params: {}, }) .catch((error) => { toast.error(`${error.message}`, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) }) setVendingMinterFlexCreationFee(vendingFactoryFlexParameters?.params?.creation_fee?.amount) setMinimumFlexMintPrice(vendingFactoryFlexParameters?.params?.min_mint_price?.amount) } if (OPEN_EDITION_FACTORY_ADDRESS) { const openEditionFactoryParameters = await client .queryContractSmart(OPEN_EDITION_FACTORY_ADDRESS, { params: {} }) .catch((error) => { toast.error(`${error.message}`, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) }) setOpenEditionMinterCreationFee(openEditionFactoryParameters?.params?.creation_fee?.amount) setMinimumOpenEditionMintPrice(openEditionFactoryParameters?.params?.min_mint_price?.amount) } if (OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS) { const openEditionUpdatableFactoryParameters = await client .queryContractSmart(OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS, { params: {} }) .catch((error) => { toast.error(`${error.message}`, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) }) setOpenEditionMinterUpdatableCreationFee(openEditionUpdatableFactoryParameters?.params?.creation_fee?.amount) setMinimumOpenEditionMintPrice(openEditionUpdatableFactoryParameters?.params?.min_mint_price?.amount) } } const checkwalletBalance = () => { if (!wallet.initialized) throw new Error('Wallet not connected.') if (minterType === 'vending' && whitelistDetails?.whitelistState === 'new' && whitelistDetails.memberLimit) { const amountNeeded = Math.ceil(Number(whitelistDetails.memberLimit) / 1000) * 100000000 + (whitelistDetails.whitelistType === 'flex' ? Number(vendingMinterFlexCreationFee) : collectionDetails?.updatable ? Number(vendingMinterUpdatableCreationFee) : Number(vendingMinterCreationFee)) if (amountNeeded >= Number(wallet.balance[0].amount)) throw new Error( `Insufficient wallet balance to instantiate the required contracts. Needed amount: ${( amountNeeded / 1000000 ).toString()} STARS`, ) } else { const amountNeeded = minterType === 'vending' ? whitelistDetails?.whitelistState === 'existing' && whitelistDetails.whitelistType === 'flex' ? Number(vendingMinterFlexCreationFee) : collectionDetails?.updatable ? Number(vendingMinterUpdatableCreationFee) : Number(vendingMinterCreationFee) : collectionDetails?.updatable ? Number(baseMinterUpdatableCreationFee) : Number(baseMinterCreationFee) if (amountNeeded >= Number(wallet.balance[0].amount)) throw new Error( `Insufficient wallet balance to instantiate the required contracts. Needed amount: ${( amountNeeded / 1000000 ).toString()} STARS`, ) } } useEffect(() => { if ( vendingMinterContractAddress !== null || openEditionMinterDetails?.openEditionMinterContractAddress || isMintingComplete ) { scrollRef.current?.scrollIntoView({ behavior: 'smooth' }) } if ( (minterType === 'vending' && vendingMinterContractAddress !== null) || (minterType === 'openEdition' && openEditionMinterDetails?.openEditionMinterContractAddress) || (minterType === 'base' && vendingMinterContractAddress !== null && isMintingComplete) ) { if (sidetabRef.current) { setTimeout(() => { // eslint-disable-next-line @typescript-eslint/no-unsafe-call sidetabRef.current.open() }, 3000) } } }, [ vendingMinterContractAddress, openEditionMinterDetails?.openEditionMinterContractAddress, isMintingComplete, minterType, ]) useEffect(() => { setBaseTokenUri(uploadDetails?.baseTokenURI as string) setCoverImageUrl(uploadDetails?.imageUrl as string) }, [uploadDetails?.baseTokenURI, uploadDetails?.imageUrl]) useEffect(() => { resetReadyFlags() setVendingMinterContractAddress(null) setIsMintingComplete(false) }, [minterType, baseMinterDetails?.baseMinterAcquisitionMethod, uploadDetails?.uploadMethod]) useEffect(() => { void fetchFactoryParameters() }, [wallet.client]) return (

{minterType === 'base' && baseMinterDetails?.baseMinterAcquisitionMethod === 'existing' ? 'Add Token' : 'Create Collection'}

Make sure you check our{' '} documentation {' '} on how to create your collection

Open Edition Minter Contract Address:{' '} {openEditionMinterDetails?.openEditionMinterContractAddress as string}
SG721 Contract Address:{' '} {openEditionMinterDetails?.sg721ContractAddress as string}
Transaction Hash: {' '} {openEditionMinterDetails?.transactionHash} {openEditionMinterDetails?.transactionHash}
{minterType === 'vending' ? 'Base Token URI: ' : 'Token URI: '}{' '} {uploadDetails?.uploadMethod === 'new' && ( ipfs://{baseTokenUri as string} )} {uploadDetails?.uploadMethod === 'existing' && ( ipfs://{baseTokenUri?.substring(baseTokenUri.lastIndexOf('ipfs://') + 7)} )}
Transaction Hash: {' '} {transactionHash} {transactionHash}
Minter Contract Address:{' '} {baseMinterDetails?.existingBaseMinter as string}
SG721 Contract Address:{' '} {sg721ContractAddress}
Visit Your Profile View the Token(s) on Marketplace
Minter Contract Address:{' '} {vendingMinterContractAddress}
SG721 Contract Address:{' '} {sg721ContractAddress}
Whitelist Contract Address:{' '} {whitelistContractAddress}
Transaction Hash: {' '} {transactionHash} {transactionHash}
Visit Your Profile View the Collection on Marketplace
{minterType === 'base' && (
)}
0 && mintingDetails.unitPrice === '0' } > Setting the unit price as 0 for public minting may render the collection vulnerable for bot attacks. Please consider creating a whitelist of addresses that can mint for free instead.
) } export default withMetadata(CollectionCreationPage, { center: false })