diff --git a/components/AssetsPreview.tsx b/components/AssetsPreview.tsx index 4056657..d0ba432 100644 --- a/components/AssetsPreview.tsx +++ b/components/AssetsPreview.tsx @@ -2,14 +2,18 @@ import clsx from 'clsx' import { useCallback, useMemo, useState } from 'react' import { getAssetType } from 'utils/getAssetType' +import type { MinterType } from './collections/actions/Combobox' +import { Conditional } from './Conditional' + interface AssetsPreviewProps { assetFilesArray: File[] updateMetadataFileIndex: (index: number) => void + minterType: MinterType } const ITEM_NUMBER = 12 -export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: AssetsPreviewProps) => { +export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex, minterType }: AssetsPreviewProps) => { const [page, setPage] = useState(1) const totalPages = useMemo(() => Math.ceil(assetFilesArray.length / ITEM_NUMBER), [assetFilesArray]) @@ -116,23 +120,25 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse return (
{renderImages()}
-
- - - - - -
+ +
+ + + + + +
+
) } diff --git a/components/ConfirmationModal.tsx b/components/ConfirmationModal.tsx index 82ceb70..edaf792 100644 --- a/components/ConfirmationModal.tsx +++ b/components/ConfirmationModal.tsx @@ -40,7 +40,7 @@ export const ConfirmationModal = (props: ConfirmationModalProps) => { />
- Are you sure to create a collection with the specified assets, metadata and parameters? + Are you sure to proceed with the specified assets, metadata and parameters?
- +

Though Stargaze's sg721 contract allows for off-chain metadata storage, it is recommended to use a @@ -293,9 +303,35 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {

+ +
+ +
+
+
+
+ +
+

+ Though Stargaze's sg721 contract allows for off-chain metadata storage, it is recommended to use a + decentralized storage solution, such as IPFS.
You may head over to{' '} + + NFT.Storage + {' '} + or{' '} + + Pinata + {' '} + and upload your asset & metadata manually to get a URI for your token before minting. +

- +
+ +
+ +
+
@@ -390,8 +426,9 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => { 'before:absolute before:inset-0 before:hover:bg-white/5 before:transition', )} id="assetFiles" - multiple + multiple={minterType === 'vending'} onChange={selectAssets} + ref={assetFilesRef} type="file" />
@@ -418,8 +455,9 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => { 'before:absolute before:inset-0 before:hover:bg-white/5 before:transition', )} id="metadataFiles" - multiple + multiple={minterType === 'vending'} onChange={selectMetadata} + ref={metadataFilesRef} type="file" /> @@ -435,7 +473,11 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => { 0}> - + diff --git a/pages/collections/create.tsx b/pages/collections/create.tsx index 07f3c8f..d6c31bb 100644 --- a/pages/collections/create.tsx +++ b/pages/collections/create.tsx @@ -26,8 +26,10 @@ import { Conditional } from 'components/Conditional' import { LoadingModal } from 'components/LoadingModal' import { useContracts } from 'contexts/contracts' import { useWallet } from 'contexts/wallet' -import type { DispatchExecuteArgs } from 'contracts/vendingFactory/messages/execute' -import { dispatchExecute } from 'contracts/vendingFactory/messages/execute' +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' @@ -35,6 +37,7 @@ import { toast } from 'react-hot-toast' import { upload } from 'services/upload' import { compareFileArrays } from 'utils/compareFileArrays' import { + BASE_FACTORY_ADDRESS, BLOCK_EXPLORER_URL, NETWORK, SG721_CODE_ID, @@ -53,17 +56,24 @@ import { getAssetType } from '../../utils/getAssetType' const CollectionCreationPage: NextPage = () => { const wallet = useWallet() const { + baseMinter: baseMinterContract, vendingMinter: vendingMinterContract, whitelist: whitelistContract, vendingFactory: vendingFactoryContract, + baseFactory: baseFactoryContract, } = useContracts() const scrollRef = useRef(null) - const messages = useMemo( + 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 [minterDetails, setMinterDetails] = useState(null) @@ -109,9 +119,8 @@ const CollectionCreationPage: NextPage = () => { try { setReadyToCreateBm(false) checkUploadDetails() - checkCollectionDetails() - checkMintingDetails() checkRoyaltyDetails() + checkCollectionDetails() checkWhitelistDetails() .then(() => { setReadyToCreateBm(true) @@ -130,9 +139,6 @@ const CollectionCreationPage: NextPage = () => { try { setReadyToUploadAndMint(false) checkUploadDetails() - checkCollectionDetails() - checkMintingDetails() - checkRoyaltyDetails() checkWhitelistDetails() .then(() => { setReadyToUploadAndMint(true) @@ -147,6 +153,12 @@ const CollectionCreationPage: NextPage = () => { } } + const resetReadyFlags = () => { + setReadyToCreateVm(false) + setReadyToCreateBm(false) + setReadyToUploadAndMint(false) + } + const createVendingMinterCollection = async () => { try { setCreatingCollection(true) @@ -180,7 +192,7 @@ const CollectionCreationPage: NextPage = () => { else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() setWhitelistContractAddress(whitelist as string) - await instantiate(baseUri, coverImageUri, whitelist) + await instantiateVendingMinter(baseUri, coverImageUri, whitelist) } else { setBaseTokenUri(uploadDetails?.baseTokenURI as string) setCoverImageUrl(uploadDetails?.imageUrl as string) @@ -190,7 +202,7 @@ const CollectionCreationPage: NextPage = () => { else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() setWhitelistContractAddress(whitelist as string) - await instantiate(baseTokenUri as string, coverImageUrl as string, whitelist) + await instantiateVendingMinter(baseTokenUri as string, coverImageUrl as string, whitelist) } setCreatingCollection(false) // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -229,22 +241,12 @@ const CollectionCreationPage: NextPage = () => { setBaseTokenUri(baseUri) setCoverImageUrl(coverImageUri) - let whitelist: string | undefined - if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress - else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() - setWhitelistContractAddress(whitelist as string) - - await instantiate(baseUri, coverImageUri, whitelist) + await instantiateBaseMinter(baseUri, coverImageUri) } else { setBaseTokenUri(uploadDetails?.baseTokenURI as string) setCoverImageUrl(uploadDetails?.imageUrl as string) - let whitelist: string | undefined - if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress - else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() - setWhitelistContractAddress(whitelist as string) - - await instantiate(baseTokenUri as string, coverImageUrl as string, whitelist) + await instantiateBaseMinter(uploadDetails?.baseTokenURI as string, uploadDetails?.imageUrl as string) } setCreatingCollection(false) // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -257,48 +259,49 @@ const CollectionCreationPage: NextPage = () => { const uploadAndMint = async () => { try { + if (!wallet.initialized) throw new Error('Wallet not connected') + if (!baseMinterContract) throw new Error('Contract not found') 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?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress - else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() - setWhitelistContractAddress(whitelist as string) - - await instantiate(baseUri, coverImageUri, whitelist) + await uploadFiles() + .then(async (baseUri) => { + setUploading(false) + setBaseTokenUri(baseUri) + const result = await baseMinterContract + .use(minterDetails?.existingMinter as string) + ?.mint(wallet.address, `ipfs://${baseUri}`) + console.log(result) + return result + }) + .then((result) => { + toast.success(`Minted successfully! Tx Hash: ${result}`, { style: { maxWidth: 'none' }, duration: 5000 }) + }) + .catch((error) => { + toast.error(error.message, { style: { maxWidth: 'none' } }) + setUploading(false) + setCreatingCollection(false) + }) } else { setBaseTokenUri(uploadDetails?.baseTokenURI as string) - setCoverImageUrl(uploadDetails?.imageUrl as string) - - let whitelist: string | undefined - if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress - else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() - setWhitelistContractAddress(whitelist as string) - - await instantiate(baseTokenUri as string, coverImageUrl as string, whitelist) + setUploading(false) + await baseMinterContract + .use(minterDetails?.existingMinter as string) + ?.mint(wallet.address, `ipfs://${uploadDetails?.baseTokenURI}`) + .then((result) => { + toast.success(`Minted successfully! Tx Hash: ${result}`, { style: { maxWidth: 'none' }, duration: 5000 }) + }) + .catch((error) => { + toast.error(error.message, { style: { maxWidth: 'none' } }) + setUploading(false) + setCreatingCollection(false) + }) } setCreatingCollection(false) // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -332,9 +335,9 @@ const CollectionCreationPage: NextPage = () => { return data.contractAddress } - const instantiate = async (baseUri: string, coverImageUri: string, whitelist?: string) => { + const instantiateVendingMinter = async (baseUri: string, coverImageUri: string, whitelist?: string) => { if (!wallet.initialized) throw new Error('Wallet not connected') - if (!vendingMinterContract) throw new Error('Contract not found') + if (!vendingFactoryContract) throw new Error('Contract not found') let royaltyInfo = null if (royaltyDetails?.royaltyType === 'new') { @@ -378,19 +381,68 @@ const CollectionCreationPage: NextPage = () => { }, } - const payload: DispatchExecuteArgs = { + const payload: VendingFactoryDispatchExecuteArgs = { contract: VENDING_FACTORY_ADDRESS, - messages, + messages: vendingFactoryMessages, txSigner: wallet.address, msg, funds: [coin('1000000000', 'ustars')], } - const data = await dispatchExecute(payload) + 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') + + let royaltyInfo = null + if (royaltyDetails?.royaltyType === 'new') { + royaltyInfo = { + payment_address: royaltyDetails.paymentAddress, + share: (Number(royaltyDetails.share) / 100).toString(), + } + } + + const msg = { + create_minter: { + init_msg: null, + collection_params: { + 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: BaseFactoryDispatchExecuteArgs = { + contract: BASE_FACTORY_ADDRESS, + messages: baseFactoryMessages, + txSigner: wallet.address, + msg, + funds: [coin('1000000000', 'ustars')], + } + const data = await baseFactoryDispatchExecute(payload) + setTransactionHash(data.transactionHash) + setVendingMinterContractAddress(data.baseMinterAddress) + setSg721ContractAddress(data.sg721Address) + } + const uploadFiles = async (): Promise => { if (!uploadDetails) throw new Error('Please upload asset and metadata') return new Promise((resolve, reject) => { @@ -459,6 +511,9 @@ const CollectionCreationPage: NextPage = () => { if (!uploadDetails) { throw new Error('Please select assets and metadata') } + if (minterType === 'base' && uploadDetails.uploadMethod === 'new' && uploadDetails.assetFiles.length > 1) { + throw new Error('Base Minter can only mint one asset at a time. Please select only one asset.') + } if (uploadDetails.uploadMethod === 'new' && uploadDetails.assetFiles.length === 0) { throw new Error('Please select the assets') } @@ -478,6 +533,9 @@ const CollectionCreationPage: NextPage = () => { if (uploadDetails.uploadMethod === 'existing' && !uploadDetails.baseTokenURI?.includes('ipfs://')) { throw new Error('Please specify a valid base token URI') } + if (minterDetails?.minterAcquisitionMethod === 'existing' && !minterDetails.existingMinter) { + throw new Error('Please specify a valid Base Minter contract address') + } } const checkCollectionDetails = () => { @@ -565,12 +623,27 @@ const CollectionCreationPage: NextPage = () => { setCoverImageUrl(uploadDetails?.imageUrl as string) }, [uploadDetails?.baseTokenURI, uploadDetails?.imageUrl]) + useEffect(() => { + resetReadyFlags() + setVendingMinterContractAddress(null) + }, [minterType, minterDetails?.minterAcquisitionMethod]) + return (
- +
-

Create Collection

+

+ {minterType === 'base' && minterDetails?.minterAcquisitionMethod === 'existing' + ? 'Mint Token' + : 'Create Collection'} +

@@ -689,7 +762,10 @@ const CollectionCreationPage: NextPage = () => { >