Implement 1/1 minting UI

This commit is contained in:
Serkan Reis 2022-12-13 21:50:42 +03:00
parent 5c6c87eb9e
commit fe4da95566
5 changed files with 230 additions and 99 deletions

View File

@ -2,14 +2,18 @@ import clsx from 'clsx'
import { useCallback, useMemo, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
import { getAssetType } from 'utils/getAssetType' import { getAssetType } from 'utils/getAssetType'
import type { MinterType } from './collections/actions/Combobox'
import { Conditional } from './Conditional'
interface AssetsPreviewProps { interface AssetsPreviewProps {
assetFilesArray: File[] assetFilesArray: File[]
updateMetadataFileIndex: (index: number) => void updateMetadataFileIndex: (index: number) => void
minterType: MinterType
} }
const ITEM_NUMBER = 12 const ITEM_NUMBER = 12
export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: AssetsPreviewProps) => { export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex, minterType }: AssetsPreviewProps) => {
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const totalPages = useMemo(() => Math.ceil(assetFilesArray.length / ITEM_NUMBER), [assetFilesArray]) const totalPages = useMemo(() => Math.ceil(assetFilesArray.length / ITEM_NUMBER), [assetFilesArray])
@ -116,6 +120,7 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
return ( return (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="mt-2 w-[400px] h-[300px]">{renderImages()}</div> <div className="mt-2 w-[400px] h-[300px]">{renderImages()}</div>
<Conditional test={minterType === 'vending'}>
<div className="mt-5 btn-group"> <div className="mt-5 btn-group">
<button className="text-white bg-plumbus-light btn" onClick={multiplePrevPage} type="button"> <button className="text-white bg-plumbus-light btn" onClick={multiplePrevPage} type="button">
«« ««
@ -133,6 +138,7 @@ export const AssetsPreview = ({ assetFilesArray, updateMetadataFileIndex }: Asse
»» »»
</button> </button>
</div> </div>
</Conditional>
</div> </div>
) )
} }

View File

@ -40,7 +40,7 @@ export const ConfirmationModal = (props: ConfirmationModalProps) => {
/> />
</div> </div>
<br /> <br />
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?
</div> </div>
<div className="flex justify-end w-full"> <div className="flex justify-end w-full">
<Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-600"> <Button className="px-0 mt-4 mr-5 mb-4 max-h-12 bg-gray-600 hover:bg-gray-600">

View File

@ -35,7 +35,7 @@ export const MinterDetails = ({ onChange, minterType }: MinterDetailsProps) => {
const wallet = useWallet() const wallet = useWallet()
const [myBaseMinterContracts, setMyBaseMinterContracts] = useState<MinterInfo[]>([]) const [myBaseMinterContracts, setMyBaseMinterContracts] = useState<MinterInfo[]>([])
const [minterAcquisitionMethod, setMinterAcquisitionMethod] = useState<MinterAcquisitionMethod>('existing') const [minterAcquisitionMethod, setMinterAcquisitionMethod] = useState<MinterAcquisitionMethod>('new')
const existingMinterState = useInputState({ const existingMinterState = useInputState({
id: 'existingMinter', id: 'existingMinter',
@ -79,7 +79,7 @@ export const MinterDetails = ({ onChange, minterType }: MinterDetailsProps) => {
minterContracts.map(async (minterContract: any) => { minterContracts.map(async (minterContract: any) => {
await getMinterContractType(minterContract.minter) await getMinterContractType(minterContract.minter)
.then((contractType) => { .then((contractType) => {
if (contractType?.includes('sg-minter')) { if (contractType?.includes('sg-base-minter')) {
setMyBaseMinterContracts((prevState) => [...prevState, minterContract]) setMyBaseMinterContracts((prevState) => [...prevState, minterContract])
} }
}) })
@ -181,12 +181,12 @@ export const MinterDetails = ({ onChange, minterType }: MinterDetailsProps) => {
<select <select
className="mt-8 w-full max-w-lg text-sm bg-white/10 select select-bordered" className="mt-8 w-full max-w-lg text-sm bg-white/10 select select-bordered"
onChange={(e) => { onChange={(e) => {
existingMinterState.onChange(e.target.value.slice(e.target.value.indexOf('-') + 2)) existingMinterState.onChange(e.target.value.slice(e.target.value.indexOf('stars1')))
e.preventDefault() e.preventDefault()
}} }}
> >
<option className="mt-2 text-lg bg-[#1A1A1A]" disabled selected> <option className="mt-2 text-lg bg-[#1A1A1A]" disabled selected>
Select a Base Minter Contract Select one of your existing Base Minter Contracts
</option> </option>
{renderMinterContracts()} {renderMinterContracts()}
</select> </select>

View File

@ -11,15 +11,20 @@ import { TextInput } from 'components/forms/FormInput'
import { useInputState } from 'components/forms/FormInput.hooks' import { useInputState } from 'components/forms/FormInput.hooks'
import { MetadataModal } from 'components/MetadataModal' import { MetadataModal } from 'components/MetadataModal'
import type { ChangeEvent } from 'react' import type { ChangeEvent } from 'react'
import { useEffect, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import type { UploadServiceType } from 'services/upload' import type { UploadServiceType } from 'services/upload'
import { naturalCompare } from 'utils/sort' import { naturalCompare } from 'utils/sort'
import type { MinterType } from '../actions/Combobox'
import type { MinterAcquisitionMethod } from './MinterDetails'
export type UploadMethod = 'new' | 'existing' export type UploadMethod = 'new' | 'existing'
interface UploadDetailsProps { interface UploadDetailsProps {
onChange: (value: UploadDetailsDataProps) => void onChange: (value: UploadDetailsDataProps) => void
minterType: MinterType
minterAcquisitionMethod?: MinterAcquisitionMethod
} }
export interface UploadDetailsDataProps { export interface UploadDetailsDataProps {
@ -34,7 +39,7 @@ export interface UploadDetailsDataProps {
imageUrl?: string imageUrl?: string
} }
export const UploadDetails = ({ onChange }: UploadDetailsProps) => { export const UploadDetails = ({ onChange, minterType, minterAcquisitionMethod }: UploadDetailsProps) => {
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([]) const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([]) const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new') const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
@ -42,6 +47,9 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0) const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0)
const [refreshMetadata, setRefreshMetadata] = useState(false) const [refreshMetadata, setRefreshMetadata] = useState(false)
const assetFilesRef = useRef<HTMLInputElement | null>(null)
const metadataFilesRef = useRef<HTMLInputElement | null>(null)
const nftStorageApiKeyState = useInputState({ const nftStorageApiKeyState = useInputState({
id: 'nft-storage-api-key', id: 'nft-storage-api-key',
name: 'nftStorageApiKey', name: 'nftStorageApiKey',
@ -67,7 +75,7 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
const baseTokenUriState = useInputState({ const baseTokenUriState = useInputState({
id: 'baseTokenUri', id: 'baseTokenUri',
name: 'baseTokenUri', name: 'baseTokenUri',
title: 'Base Token URI', title: minterType === 'vending' ? 'Base Token URI' : 'Token URI',
placeholder: 'ipfs://', placeholder: 'ipfs://',
defaultValue: '', defaultValue: '',
}) })
@ -226,11 +234,13 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
]) ])
useEffect(() => { useEffect(() => {
if (assetFilesRef.current) assetFilesRef.current.value = ''
if (assetFilesRef.current) assetFilesRef.current.value = ''
setAssetFilesArray([]) setAssetFilesArray([])
setMetadataFilesArray([]) setMetadataFilesArray([])
baseTokenUriState.onChange('') baseTokenUriState.onChange('')
coverImageUrlState.onChange('') coverImageUrlState.onChange('')
}, [uploadMethod]) }, [uploadMethod, minterType, minterAcquisitionMethod])
return ( return (
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column"> <div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
@ -251,7 +261,7 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label" className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio2" htmlFor="inlineRadio2"
> >
Upload assets & metadata {minterType === 'base' ? 'Upload asset & metadata' : 'Upload assets & metadata'}
</label> </label>
</div> </div>
<div className="mt-3 ml-2 font-bold form-check form-check-inline"> <div className="mt-3 ml-2 font-bold form-check form-check-inline">
@ -270,13 +280,13 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label" className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
htmlFor="inlineRadio1" htmlFor="inlineRadio1"
> >
Use an existing base URI {minterType === 'base' ? 'Use an existing Token URI' : 'Use an existing base URI'}
</label> </label>
</div> </div>
</div> </div>
<div className="p-3 py-5 pb-8"> <div className="p-3 py-5 pb-8">
<Conditional test={uploadMethod === 'existing'}> <Conditional test={uploadMethod === 'existing' && minterType === 'vending'}>
<div className="ml-3 flex-column"> <div className="ml-3 flex-column">
<p className="mb-5 ml-5"> <p className="mb-5 ml-5">
Though Stargaze&apos;s sg721 contract allows for off-chain metadata storage, it is recommended to use a Though Stargaze&apos;s sg721 contract allows for off-chain metadata storage, it is recommended to use a
@ -293,9 +303,35 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
<div> <div>
<TextInput {...baseTokenUriState} className="w-1/2" /> <TextInput {...baseTokenUriState} className="w-1/2" />
</div> </div>
<Conditional test={minterType !== 'base'}>
<div> <div>
<TextInput {...coverImageUrlState} className="mt-2 w-1/2" /> <TextInput {...coverImageUrlState} className="mt-2 w-1/2" />
</div> </div>
</Conditional>
</div>
</Conditional>
<Conditional test={uploadMethod === 'existing' && minterType === 'base'}>
<div className="ml-3 flex-column">
<p className="mb-5 ml-5">
Though Stargaze&apos;s sg721 contract allows for off-chain metadata storage, it is recommended to use a
decentralized storage solution, such as IPFS. <br /> You may head over to{' '}
<Anchor className="font-bold text-plumbus hover:underline" href="https://nft.storage">
NFT.Storage
</Anchor>{' '}
or{' '}
<Anchor className="font-bold text-plumbus hover:underline" href="https://www.pinata.cloud/">
Pinata
</Anchor>{' '}
and upload your asset & metadata manually to get a URI for your token before minting.
</p>
<div>
<TextInput {...baseTokenUriState} className="w-1/2" />
</div>
<Conditional test={minterType !== 'base' || (minterType === 'base' && minterAcquisitionMethod === 'new')}>
<div>
<TextInput {...coverImageUrlState} className="mt-2 w-1/2" />
</div>
</Conditional>
</div> </div>
</Conditional> </Conditional>
<Conditional test={uploadMethod === 'new'}> <Conditional test={uploadMethod === 'new'}>
@ -390,8 +426,9 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition', 'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)} )}
id="assetFiles" id="assetFiles"
multiple multiple={minterType === 'vending'}
onChange={selectAssets} onChange={selectAssets}
ref={assetFilesRef}
type="file" type="file"
/> />
</div> </div>
@ -418,8 +455,9 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition', 'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
)} )}
id="metadataFiles" id="metadataFiles"
multiple multiple={minterType === 'vending'}
onChange={selectMetadata} onChange={selectMetadata}
ref={metadataFilesRef}
type="file" type="file"
/> />
</div> </div>
@ -435,7 +473,11 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
</div> </div>
<Conditional test={assetFilesArray.length > 0}> <Conditional test={assetFilesArray.length > 0}>
<AssetsPreview assetFilesArray={assetFilesArray} updateMetadataFileIndex={updateMetadataFileIndex} /> <AssetsPreview
assetFilesArray={assetFilesArray}
minterType={minterType}
updateMetadataFileIndex={updateMetadataFileIndex}
/>
</Conditional> </Conditional>
</div> </div>
</div> </div>

View File

@ -26,8 +26,10 @@ import { Conditional } from 'components/Conditional'
import { LoadingModal } from 'components/LoadingModal' import { LoadingModal } from 'components/LoadingModal'
import { useContracts } from 'contexts/contracts' import { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet' import { useWallet } from 'contexts/wallet'
import type { DispatchExecuteArgs } from 'contracts/vendingFactory/messages/execute' import type { DispatchExecuteArgs as BaseFactoryDispatchExecuteArgs } from 'contracts/baseFactory/messages/execute'
import { dispatchExecute } from 'contracts/vendingFactory/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 type { NextPage } from 'next'
import { NextSeo } from 'next-seo' import { NextSeo } from 'next-seo'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
@ -35,6 +37,7 @@ import { toast } from 'react-hot-toast'
import { upload } from 'services/upload' import { upload } from 'services/upload'
import { compareFileArrays } from 'utils/compareFileArrays' import { compareFileArrays } from 'utils/compareFileArrays'
import { import {
BASE_FACTORY_ADDRESS,
BLOCK_EXPLORER_URL, BLOCK_EXPLORER_URL,
NETWORK, NETWORK,
SG721_CODE_ID, SG721_CODE_ID,
@ -53,17 +56,24 @@ import { getAssetType } from '../../utils/getAssetType'
const CollectionCreationPage: NextPage = () => { const CollectionCreationPage: NextPage = () => {
const wallet = useWallet() const wallet = useWallet()
const { const {
baseMinter: baseMinterContract,
vendingMinter: vendingMinterContract, vendingMinter: vendingMinterContract,
whitelist: whitelistContract, whitelist: whitelistContract,
vendingFactory: vendingFactoryContract, vendingFactory: vendingFactoryContract,
baseFactory: baseFactoryContract,
} = useContracts() } = useContracts()
const scrollRef = useRef<HTMLDivElement>(null) const scrollRef = useRef<HTMLDivElement>(null)
const messages = useMemo( const vendingFactoryMessages = useMemo(
() => vendingFactoryContract?.use(VENDING_FACTORY_ADDRESS), () => vendingFactoryContract?.use(VENDING_FACTORY_ADDRESS),
[vendingFactoryContract, wallet.address], [vendingFactoryContract, wallet.address],
) )
const baseFactoryMessages = useMemo(
() => baseFactoryContract?.use(BASE_FACTORY_ADDRESS),
[baseFactoryContract, wallet.address],
)
const [uploadDetails, setUploadDetails] = useState<UploadDetailsDataProps | null>(null) const [uploadDetails, setUploadDetails] = useState<UploadDetailsDataProps | null>(null)
const [collectionDetails, setCollectionDetails] = useState<CollectionDetailsDataProps | null>(null) const [collectionDetails, setCollectionDetails] = useState<CollectionDetailsDataProps | null>(null)
const [minterDetails, setMinterDetails] = useState<MinterDetailsDataProps | null>(null) const [minterDetails, setMinterDetails] = useState<MinterDetailsDataProps | null>(null)
@ -109,9 +119,8 @@ const CollectionCreationPage: NextPage = () => {
try { try {
setReadyToCreateBm(false) setReadyToCreateBm(false)
checkUploadDetails() checkUploadDetails()
checkCollectionDetails()
checkMintingDetails()
checkRoyaltyDetails() checkRoyaltyDetails()
checkCollectionDetails()
checkWhitelistDetails() checkWhitelistDetails()
.then(() => { .then(() => {
setReadyToCreateBm(true) setReadyToCreateBm(true)
@ -130,9 +139,6 @@ const CollectionCreationPage: NextPage = () => {
try { try {
setReadyToUploadAndMint(false) setReadyToUploadAndMint(false)
checkUploadDetails() checkUploadDetails()
checkCollectionDetails()
checkMintingDetails()
checkRoyaltyDetails()
checkWhitelistDetails() checkWhitelistDetails()
.then(() => { .then(() => {
setReadyToUploadAndMint(true) setReadyToUploadAndMint(true)
@ -147,6 +153,12 @@ const CollectionCreationPage: NextPage = () => {
} }
} }
const resetReadyFlags = () => {
setReadyToCreateVm(false)
setReadyToCreateBm(false)
setReadyToUploadAndMint(false)
}
const createVendingMinterCollection = async () => { const createVendingMinterCollection = async () => {
try { try {
setCreatingCollection(true) setCreatingCollection(true)
@ -180,7 +192,7 @@ const CollectionCreationPage: NextPage = () => {
else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist()
setWhitelistContractAddress(whitelist as string) setWhitelistContractAddress(whitelist as string)
await instantiate(baseUri, coverImageUri, whitelist) await instantiateVendingMinter(baseUri, coverImageUri, whitelist)
} else { } else {
setBaseTokenUri(uploadDetails?.baseTokenURI as string) setBaseTokenUri(uploadDetails?.baseTokenURI as string)
setCoverImageUrl(uploadDetails?.imageUrl as string) setCoverImageUrl(uploadDetails?.imageUrl as string)
@ -190,7 +202,7 @@ const CollectionCreationPage: NextPage = () => {
else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist()
setWhitelistContractAddress(whitelist as string) setWhitelistContractAddress(whitelist as string)
await instantiate(baseTokenUri as string, coverImageUrl as string, whitelist) await instantiateVendingMinter(baseTokenUri as string, coverImageUrl as string, whitelist)
} }
setCreatingCollection(false) setCreatingCollection(false)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -229,22 +241,12 @@ const CollectionCreationPage: NextPage = () => {
setBaseTokenUri(baseUri) setBaseTokenUri(baseUri)
setCoverImageUrl(coverImageUri) setCoverImageUrl(coverImageUri)
let whitelist: string | undefined await instantiateBaseMinter(baseUri, coverImageUri)
if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress
else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist()
setWhitelistContractAddress(whitelist as string)
await instantiate(baseUri, coverImageUri, whitelist)
} else { } else {
setBaseTokenUri(uploadDetails?.baseTokenURI as string) setBaseTokenUri(uploadDetails?.baseTokenURI as string)
setCoverImageUrl(uploadDetails?.imageUrl as string) setCoverImageUrl(uploadDetails?.imageUrl as string)
let whitelist: string | undefined await instantiateBaseMinter(uploadDetails?.baseTokenURI as string, uploadDetails?.imageUrl as string)
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)
} }
setCreatingCollection(false) setCreatingCollection(false)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -257,48 +259,49 @@ const CollectionCreationPage: NextPage = () => {
const uploadAndMint = async () => { const uploadAndMint = async () => {
try { try {
if (!wallet.initialized) throw new Error('Wallet not connected')
if (!baseMinterContract) throw new Error('Contract not found')
setCreatingCollection(true) setCreatingCollection(true)
setBaseTokenUri(null) setBaseTokenUri(null)
setCoverImageUrl(null) setCoverImageUrl(null)
setVendingMinterContractAddress(null) setVendingMinterContractAddress(null)
setSg721ContractAddress(null) setSg721ContractAddress(null)
setWhitelistContractAddress(null)
setTransactionHash(null) setTransactionHash(null)
if (uploadDetails?.uploadMethod === 'new') { if (uploadDetails?.uploadMethod === 'new') {
setUploading(true) setUploading(true)
await uploadFiles()
const baseUri = await uploadFiles() .then(async (baseUri) => {
//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) setUploading(false)
setBaseTokenUri(baseUri) setBaseTokenUri(baseUri)
setCoverImageUrl(coverImageUri) const result = await baseMinterContract
.use(minterDetails?.existingMinter as string)
let whitelist: string | undefined ?.mint(wallet.address, `ipfs://${baseUri}`)
if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress console.log(result)
else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() return result
setWhitelistContractAddress(whitelist as string) })
.then((result) => {
await instantiate(baseUri, coverImageUri, whitelist) 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 { } else {
setBaseTokenUri(uploadDetails?.baseTokenURI as string) setBaseTokenUri(uploadDetails?.baseTokenURI as string)
setCoverImageUrl(uploadDetails?.imageUrl as string) setUploading(false)
await baseMinterContract
let whitelist: string | undefined .use(minterDetails?.existingMinter as string)
if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress ?.mint(wallet.address, `ipfs://${uploadDetails?.baseTokenURI}`)
else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() .then((result) => {
setWhitelistContractAddress(whitelist as string) toast.success(`Minted successfully! Tx Hash: ${result}`, { style: { maxWidth: 'none' }, duration: 5000 })
})
await instantiate(baseTokenUri as string, coverImageUrl as string, whitelist) .catch((error) => {
toast.error(error.message, { style: { maxWidth: 'none' } })
setUploading(false)
setCreatingCollection(false)
})
} }
setCreatingCollection(false) setCreatingCollection(false)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -332,9 +335,9 @@ const CollectionCreationPage: NextPage = () => {
return data.contractAddress 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 (!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 let royaltyInfo = null
if (royaltyDetails?.royaltyType === 'new') { if (royaltyDetails?.royaltyType === 'new') {
@ -378,19 +381,68 @@ const CollectionCreationPage: NextPage = () => {
}, },
} }
const payload: DispatchExecuteArgs = { const payload: VendingFactoryDispatchExecuteArgs = {
contract: VENDING_FACTORY_ADDRESS, contract: VENDING_FACTORY_ADDRESS,
messages, messages: vendingFactoryMessages,
txSigner: wallet.address, txSigner: wallet.address,
msg, msg,
funds: [coin('1000000000', 'ustars')], funds: [coin('1000000000', 'ustars')],
} }
const data = await dispatchExecute(payload) const data = await vendingFactoryDispatchExecute(payload)
setTransactionHash(data.transactionHash) setTransactionHash(data.transactionHash)
setVendingMinterContractAddress(data.vendingMinterAddress) setVendingMinterContractAddress(data.vendingMinterAddress)
setSg721ContractAddress(data.sg721Address) 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<string> => { const uploadFiles = async (): Promise<string> => {
if (!uploadDetails) throw new Error('Please upload asset and metadata') if (!uploadDetails) throw new Error('Please upload asset and metadata')
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -459,6 +511,9 @@ const CollectionCreationPage: NextPage = () => {
if (!uploadDetails) { if (!uploadDetails) {
throw new Error('Please select assets and metadata') 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) { if (uploadDetails.uploadMethod === 'new' && uploadDetails.assetFiles.length === 0) {
throw new Error('Please select the assets') throw new Error('Please select the assets')
} }
@ -478,6 +533,9 @@ const CollectionCreationPage: NextPage = () => {
if (uploadDetails.uploadMethod === 'existing' && !uploadDetails.baseTokenURI?.includes('ipfs://')) { if (uploadDetails.uploadMethod === 'existing' && !uploadDetails.baseTokenURI?.includes('ipfs://')) {
throw new Error('Please specify a valid base token URI') 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 = () => { const checkCollectionDetails = () => {
@ -565,12 +623,27 @@ const CollectionCreationPage: NextPage = () => {
setCoverImageUrl(uploadDetails?.imageUrl as string) setCoverImageUrl(uploadDetails?.imageUrl as string)
}, [uploadDetails?.baseTokenURI, uploadDetails?.imageUrl]) }, [uploadDetails?.baseTokenURI, uploadDetails?.imageUrl])
useEffect(() => {
resetReadyFlags()
setVendingMinterContractAddress(null)
}, [minterType, minterDetails?.minterAcquisitionMethod])
return ( return (
<div> <div>
<NextSeo title="Create Collection" /> <NextSeo
title={
minterType === 'base' && minterDetails?.minterAcquisitionMethod === 'existing'
? 'Mint Token'
: 'Create Collection'
}
/>
<div className="mt-5 space-y-5 text-center"> <div className="mt-5 space-y-5 text-center">
<h1 className="font-heading text-4xl font-bold">Create Collection</h1> <h1 className="font-heading text-4xl font-bold">
{minterType === 'base' && minterDetails?.minterAcquisitionMethod === 'existing'
? 'Mint Token'
: 'Create Collection'}
</h1>
<Conditional test={uploading}> <Conditional test={uploading}>
<LoadingModal /> <LoadingModal />
@ -689,7 +762,10 @@ const CollectionCreationPage: NextPage = () => {
> >
<button <button
className="p-4 w-full h-full text-left bg-transparent" className="p-4 w-full h-full text-left bg-transparent"
onClick={() => setMinterType('vending')} onClick={() => {
setMinterType('vending')
resetReadyFlags()
}}
type="button" type="button"
> >
<h4 className="font-bold">Vending Minter</h4> <h4 className="font-bold">Vending Minter</h4>
@ -708,7 +784,10 @@ const CollectionCreationPage: NextPage = () => {
> >
<button <button
className="p-4 w-full h-full text-left bg-transparent" className="p-4 w-full h-full text-left bg-transparent"
onClick={() => setMinterType('base')} onClick={() => {
setMinterType('base')
resetReadyFlags()
}}
type="button" type="button"
> >
<h4 className="font-bold">Base Minter</h4> <h4 className="font-bold">Base Minter</h4>
@ -725,7 +804,11 @@ const CollectionCreationPage: NextPage = () => {
)} )}
<div className="mx-10"> <div className="mx-10">
<UploadDetails onChange={setUploadDetails} /> <UploadDetails
minterAcquisitionMethod={minterDetails?.minterAcquisitionMethod}
minterType={minterType}
onChange={setUploadDetails}
/>
<Conditional <Conditional
test={minterType === 'vending' || (minterType === 'base' && minterDetails?.minterAcquisitionMethod === 'new')} test={minterType === 'vending' || (minterType === 'base' && minterDetails?.minterAcquisitionMethod === 'new')}
@ -794,7 +877,7 @@ const CollectionCreationPage: NextPage = () => {
onClick={performBaseMinterChecks} onClick={performBaseMinterChecks}
variant="solid" variant="solid"
> >
Create BM Collection Create Collection
</Button> </Button>
</Conditional> </Conditional>
<Conditional test={minterType === 'base' && minterDetails?.minterAcquisitionMethod === 'existing'}> <Conditional test={minterType === 'base' && minterDetails?.minterAcquisitionMethod === 'existing'}>