/* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable no-nested-ternary */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { toUtf8 } from '@cosmjs/encoding' import type { Coin } from '@cosmjs/proto-signing' import { coin } from '@cosmjs/proto-signing' import axios from 'axios' import clsx from 'clsx' import { Button } from 'components/Button' import type { MinterType } from 'components/collections/actions/Combobox' import { Conditional } from 'components/Conditional' import { ConfirmationModal } from 'components/ConfirmationModal' import { LoadingModal } from 'components/LoadingModal' import { type TokenInfo, tokensList } from 'config/token' import { useContracts } from 'contexts/contracts' import { addLogItem } from 'contexts/log' import type { DispatchExecuteArgs as OpenEditionFactoryDispatchExecuteArgs } from 'contracts/openEditionFactory/messages/execute' import { dispatchExecute as openEditionFactoryDispatchExecute } from 'contracts/openEditionFactory/messages/execute' import React, { useEffect, useMemo, useState } from 'react' import { toast } from 'react-hot-toast' import { upload } from 'services/upload' import { SG721_OPEN_EDITION_CODE_ID, SG721_OPEN_EDITION_UPDATABLE_CODE_ID, STRDST_SG721_CODE_ID, WHITELIST_CODE_ID, WHITELIST_FLEX_CODE_ID, WHITELIST_MERKLE_TREE_API_URL, WHITELIST_MERKLE_TREE_CODE_ID, } from 'utils/constants' import { useDebounce } from 'utils/debounce' import type { AssetType } from 'utils/getAssetType' import { isValidAddress } from 'utils/isValidAddress' import { checkTokenUri } from 'utils/isValidTokenUri' import { uid } from 'utils/random' import { useWallet } from 'utils/wallet' import { type CollectionDetailsDataProps, CollectionDetails } from './CollectionDetails' import type { ImageUploadDetailsDataProps } from './ImageUploadDetails' import { ImageUploadDetails } from './ImageUploadDetails' import type { LimitType, MintingDetailsDataProps } from './MintingDetails' import { MintingDetails } from './MintingDetails' import type { UploadMethod } from './OffChainMetadataUploadDetails' import { type OffChainMetadataUploadDetailsDataProps, OffChainMetadataUploadDetails, } from './OffChainMetadataUploadDetails' import type { OnChainMetadataInputDetailsDataProps } from './OnChainMetadataInputDetails' import { OnChainMetadataInputDetails } from './OnChainMetadataInputDetails' import { type RoyaltyDetailsDataProps, RoyaltyDetails } from './RoyaltyDetails' import { type WhitelistDetailsDataProps, WhitelistDetails } from './WhitelistDetails' export type MetadataStorageMethod = 'off-chain' | 'on-chain' export interface OpenEditionMinterDetailsDataProps { imageUploadDetails?: ImageUploadDetailsDataProps collectionDetails?: CollectionDetailsDataProps whitelistDetails?: WhitelistDetailsDataProps royaltyDetails?: RoyaltyDetailsDataProps onChainMetadataInputDetails?: OnChainMetadataInputDetailsDataProps offChainMetadataUploadDetails?: OffChainMetadataUploadDetailsDataProps mintingDetails?: MintingDetailsDataProps metadataStorageMethod?: MetadataStorageMethod openEditionMinterContractAddress?: string | null coverImageUrl?: string | null tokenUri?: string | null tokenImageUri?: string | null isRefreshed?: boolean } interface OpenEditionMinterCreatorProps { onChange: (data: OpenEditionMinterCreatorDataProps) => void onDetailsChange: (data: OpenEditionMinterDetailsDataProps) => void openEditionMinterCreationFee?: Coin minimumMintPrice?: string minterType?: MinterType mintTokenFromFactory?: TokenInfo | undefined importedOpenEditionMinterDetails?: OpenEditionMinterDetailsDataProps isMatchingFactoryPresent?: boolean openEditionFactoryAddress?: string } export interface OpenEditionMinterCreatorDataProps { metadataStorageMethod: MetadataStorageMethod openEditionMinterContractAddress: string | null sg721ContractAddress: string | null whitelistContractAddress: string | null transactionHash: string | null } export const OpenEditionMinterCreator = ({ onChange, onDetailsChange, openEditionMinterCreationFee, minimumMintPrice, minterType, mintTokenFromFactory, importedOpenEditionMinterDetails, isMatchingFactoryPresent, openEditionFactoryAddress, }: OpenEditionMinterCreatorProps) => { const wallet = useWallet() const { openEditionMinter: openEditionMinterContract, openEditionFactory: openEditionFactoryContract, whitelist: whitelistContract, whitelistMerkleTree: whitelistMerkleTreeContract, } = useContracts() const [metadataStorageMethod, setMetadataStorageMethod] = useState('off-chain') const [imageUploadDetails, setImageUploadDetails] = useState(null) const [collectionDetails, setCollectionDetails] = useState(null) const [whitelistDetails, setWhitelistDetails] = useState(null) const [royaltyDetails, setRoyaltyDetails] = useState(null) const [isRefreshed, setIsRefreshed] = useState(false) const [onChainMetadataInputDetails, setOnChainMetadataInputDetails] = useState(null) const [offChainMetadataUploadDetails, setOffChainMetadataUploadDetails] = useState(null) const [mintingDetails, setMintingDetails] = useState(null) const [creationInProgress, setCreationInProgress] = useState(false) const [readyToCreate, setReadyToCreate] = useState(false) const [uploading, setUploading] = useState(false) const [tokenUri, setTokenUri] = useState(null) const [tokenImageUri, setTokenImageUri] = useState(null) const [coverImageUrl, setCoverImageUrl] = useState(null) const [openEditionMinterContractAddress, setOpenEditionMinterContractAddress] = useState(null) const [sg721ContractAddress, setSg721ContractAddress] = useState(null) const [whitelistContractAddress, setWhitelistContractAddress] = useState(null) const [transactionHash, setTransactionHash] = useState(null) const [thumbnailImageUri, setThumbnailImageUri] = useState(undefined) const thumbnailCompatibleAssetTypes: AssetType[] = ['video', 'audio', 'html'] const openEditionFactoryMessages = useMemo( () => openEditionFactoryContract?.use(openEditionFactoryAddress as string), [ openEditionFactoryContract, wallet.address, collectionDetails?.updatable, openEditionFactoryAddress, wallet.isWalletConnected, ], ) const performOpenEditionMinterChecks = () => { try { setReadyToCreate(false) checkCollectionDetails() checkMintingDetails() void checkUploadDetails() .then(() => { void checkRoyaltyDetails() .then(() => { checkWhitelistDetails() .then(() => { 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) => { 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() }) } setReadyToCreate(false) }) }) .catch((error: any) => { 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 Upload 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.message, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setUploading(false) setReadyToCreate(false) } } const checkUploadDetails = async () => { if (!wallet.isWalletConnected) throw new Error('Wallet not connected.') if ( (metadataStorageMethod === 'off-chain' && !offChainMetadataUploadDetails) || (metadataStorageMethod === 'on-chain' && !imageUploadDetails) ) { throw new Error('Please select assets and metadata') } if ( metadataStorageMethod === 'off-chain' && offChainMetadataUploadDetails?.uploadMethod === 'new' && offChainMetadataUploadDetails.assetFiles.length === 0 ) { throw new Error('Please select the asset file') } if ( metadataStorageMethod === 'on-chain' && imageUploadDetails?.uploadMethod === 'new' && imageUploadDetails.assetFile === undefined ) { throw new Error('Please select the asset file') } if (metadataStorageMethod === 'off-chain' && offChainMetadataUploadDetails?.uploadMethod === 'new') { if ( offChainMetadataUploadDetails.uploadService === 'nft-storage' && offChainMetadataUploadDetails.nftStorageApiKey === '' ) { throw new Error('Please enter a valid NFT.Storage API key') } else if ( offChainMetadataUploadDetails.uploadService === 'pinata' && (offChainMetadataUploadDetails.pinataApiKey === '' || offChainMetadataUploadDetails.pinataSecretKey === '') ) { throw new Error('Please enter valid Pinata API and secret keys') } } if (metadataStorageMethod === 'on-chain' && imageUploadDetails?.uploadMethod === 'new') { if (imageUploadDetails.uploadService === 'nft-storage' && imageUploadDetails.nftStorageApiKey === '') { throw new Error('Please enter a valid NFT.Storage API key') } else if ( imageUploadDetails.uploadService === 'pinata' && (imageUploadDetails.pinataApiKey === '' || imageUploadDetails.pinataSecretKey === '') ) { throw new Error('Please enter valid Pinata API and secret keys') } } if (metadataStorageMethod === 'off-chain' && offChainMetadataUploadDetails?.uploadMethod === 'existing') { if ( offChainMetadataUploadDetails.tokenURI === '' || !(offChainMetadataUploadDetails.tokenURI as string).includes('ipfs://') ) { throw new Error('Please enter a valid token URI') } if ( offChainMetadataUploadDetails.imageUrl === '' || !(offChainMetadataUploadDetails.imageUrl as string).includes('ipfs://') ) { throw new Error('Please enter a valid image URI') } } if (metadataStorageMethod === 'on-chain' && imageUploadDetails?.uploadMethod === 'existing') { if (imageUploadDetails.imageUrl === '' || !(imageUploadDetails.imageUrl as string).includes('ipfs://')) { throw new Error('Please enter a valid asset URI') } if ( imageUploadDetails.coverImageUrl === '' || !(imageUploadDetails.coverImageUrl as string).includes('ipfs://') ) { throw new Error('Please enter a valid cover image URL') } } if (offChainMetadataUploadDetails?.uploadMethod === 'existing') { await checkTokenUri(offChainMetadataUploadDetails.tokenURI 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') if (collectionDetails.description === '') throw new Error('Collection description is required') if (collectionDetails.symbol === '') throw new Error('Collection symbol is required') if (collectionDetails.description.length > 512) throw new Error('Collection description cannot exceed 512 characters') if ( metadataStorageMethod === 'off-chain' && offChainMetadataUploadDetails?.uploadMethod === 'new' && collectionDetails.imageFile.length === 0 ) throw new Error('Collection cover image is required') if ( metadataStorageMethod === 'on-chain' && imageUploadDetails?.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.unitPrice === '') throw new Error('Mint price is required') if (collectionDetails?.updatable) { if (Number(mintingDetails.unitPrice) < Number(minimumMintPrice)) throw new Error( `Invalid mint price: The minimum mint price is ${Number(minimumMintPrice) / 1000000} ${ mintTokenFromFactory?.displayName }`, ) } else if (Number(mintingDetails.unitPrice) < Number(minimumMintPrice)) throw new Error( `Invalid mint price: The minimum mint price is ${Number(minimumMintPrice) / 1000000} ${ mintTokenFromFactory?.displayName }`, ) if (!mintingDetails.perAddressLimit || mintingDetails.perAddressLimit < 1 || mintingDetails.perAddressLimit > 50) throw new Error('Invalid limit for tokens per address') if (mintingDetails.startTime === '') throw new Error('Start time is required') if (mintingDetails.limitType === 'time_limited' && mintingDetails.endTime === '') throw new Error('End time is required') if (mintingDetails.limitType === 'count_limited' && mintingDetails.tokenCountLimit === undefined) throw new Error('Token count limit is required') if ( mintingDetails.limitType === 'count_limited' && mintingDetails.perAddressLimit > (mintingDetails.tokenCountLimit as number) ) throw new Error('Per address limit cannot exceed maximum token count limit') if (mintingDetails.limitType === 'count_limited' && (mintingDetails.tokenCountLimit as number) > 10000) throw new Error('Maximum token count cannot exceed 10000') if (Number(mintingDetails.startTime) < new Date().getTime() * 1000000) throw new Error('Invalid start time') if ( mintingDetails.limitType === 'time_limited' && Number(mintingDetails.endTime) < Number(mintingDetails.startTime) ) throw new Error('End time cannot be earlier than start time') if ( mintingDetails.limitType === 'time_limited' && Number(mintingDetails.endTime) === Number(mintingDetails.startTime) ) throw new Error('End time cannot be equal to the start time') if ( mintingDetails.paymentAddress && (!isValidAddress(mintingDetails.paymentAddress) || !mintingDetails.paymentAddress.startsWith('stars1')) ) throw new Error('Invalid payment address') if (!isMatchingFactoryPresent) throw new Error( `No matching open edition factory contract found for the selected parameters (Mint Price Denom: ${mintingDetails.selectedMintToken?.displayName}, Whitelist Type: ${whitelistDetails?.whitelistType})`, ) } 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 if (!JSON.stringify(config).includes('member_limit') || config?.member_limit === 0) { // whitelistDetails.whitelistType = 'merkletree' throw new Error( 'Whitelist Merkle Tree is not supported yet. Please use a standard or flexible whitelist contract.', ) } 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?.tokenCountLimit && config?.per_address_limit) { if (mintingDetails.tokenCountLimit >= 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.tokenCountLimit >= 100 && Number(config.per_address_limit) > Math.ceil((mintingDetails.tokenCountLimit / 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.tokenCountLimit < 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 a token count limit smaller than 100 tokens.`, ) } } } } 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.whitelistType !== 'merkletree' && (!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?.tokenCountLimit) { if (mintingDetails.tokenCountLimit >= 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.tokenCountLimit >= 100 && whitelistDetails.perAddressLimit > Math.ceil((mintingDetails.tokenCountLimit / 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.tokenCountLimit < 100 && whitelistDetails.perAddressLimit > 3) { throw Error( `Invalid limit for tokens per address. Tokens per address limit cannot exceed 3 for collections with a token count limit smaller than 100 tokens.`, ) } } } } 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 (await wallet.getCosmWasmClient()) .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('minter') || contractInfo.contract.includes('sg721'))) throw new Error('The provided royalty payment address does not belong to a compatible contract.') else console.log(contractInfo) } } } const checkwalletBalance = async () => { if (!wallet.isWalletConnected) throw new Error('Wallet not connected.') const queryClient = await wallet.getCosmWasmClient() const creationFeeDenom = tokensList.find((token) => token.denom === openEditionMinterCreationFee?.denom) await queryClient.getBalance(wallet.address || '', 'ustars').then(async (starsBalance) => { await queryClient .getBalance(wallet.address || '', openEditionMinterCreationFee?.denom as string) .then((creationFeeDenomBalance) => { if (whitelistDetails?.whitelistState === 'new' && whitelistDetails.memberLimit) { const whitelistCreationFee = Math.ceil(Number(whitelistDetails.memberLimit) / 1000) * 100000000 if (openEditionMinterCreationFee?.denom === 'ustars') { const amountNeeded = whitelistCreationFee + Number(openEditionMinterCreationFee.amount) if (amountNeeded >= Number(starsBalance.amount)) throw new Error( `Insufficient wallet balance to instantiate the required contracts. Needed amount: ${( amountNeeded / 1000000 ).toString()} STARS`, ) } else { if (whitelistCreationFee >= Number(starsBalance.amount)) throw new Error( `Insufficient wallet balance to instantiate the whitelist. Needed amount: ${( whitelistCreationFee / 1000000 ).toString()} STARS`, ) if (Number(openEditionMinterCreationFee?.amount) > Number(creationFeeDenomBalance.amount)) throw new Error( `Insufficient wallet balance to instantiate the required contracts. Needed amount: ${( Number(openEditionMinterCreationFee?.amount) / 1000000 ).toString()} ${ creationFeeDenom ? creationFeeDenom.displayName : openEditionMinterCreationFee?.denom }`, ) } } else if (Number(openEditionMinterCreationFee?.amount) > Number(creationFeeDenomBalance.amount)) throw new Error( `Insufficient wallet balance to instantiate the required contracts. Needed amount: ${( Number(openEditionMinterCreationFee?.amount) / 1000000 ).toString()} ${creationFeeDenom ? creationFeeDenom.displayName : openEditionMinterCreationFee?.denom}`, ) }) }) } const createOpenEditionMinter = async () => { try { setCreationInProgress(true) setTokenUri(null) setCoverImageUrl(null) setTokenImageUri(null) setOpenEditionMinterContractAddress(null) setSg721ContractAddress(null) setWhitelistContractAddress(null) setTransactionHash(null) if (metadataStorageMethod === 'off-chain') { if (offChainMetadataUploadDetails?.uploadMethod === 'new') { setUploading(true) const metadataUri = await uploadForOffChainStorage() const coverImageUri = await upload( collectionDetails?.imageFile as File[], offChainMetadataUploadDetails.uploadService, 'cover', offChainMetadataUploadDetails.nftStorageApiKey as string, offChainMetadataUploadDetails.pinataApiKey as string, offChainMetadataUploadDetails.pinataSecretKey as string, ) const metadataUriWithBase = `ipfs://${metadataUri}/${( offChainMetadataUploadDetails.openEditionMinterMetadataFile as File ).name.substring( 0, (offChainMetadataUploadDetails.openEditionMinterMetadataFile as File).name.lastIndexOf('.'), )}` const coverImageUriWithBase = `ipfs://${coverImageUri}/${(collectionDetails?.imageFile as File[])[0].name}` setTokenUri(metadataUriWithBase) setCoverImageUrl(coverImageUriWithBase) setUploading(false) let whitelist: string | undefined if (whitelistDetails?.whitelistState === 'existing') whitelist = whitelistDetails.contractAddress else if (whitelistDetails?.whitelistState === 'new') whitelist = await instantiateWhitelist() setWhitelistContractAddress(whitelist as string) await instantiateOpenEditionMinter(metadataUriWithBase, coverImageUriWithBase, undefined, whitelist) } else { setTokenUri(offChainMetadataUploadDetails?.tokenURI as string) setCoverImageUrl(offChainMetadataUploadDetails?.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 instantiateOpenEditionMinter( offChainMetadataUploadDetails?.tokenURI as string, offChainMetadataUploadDetails?.imageUrl as string, undefined, whitelist, ) } } else if (metadataStorageMethod === 'on-chain') { if (imageUploadDetails?.uploadMethod === 'new') { setUploading(true) const imageUri = await upload( [imageUploadDetails.assetFile as File], imageUploadDetails.uploadService, 'cover', imageUploadDetails.nftStorageApiKey as string, imageUploadDetails.pinataApiKey as string, imageUploadDetails.pinataSecretKey as string, ) const imageUriWithBase = `ipfs://${imageUri}/${(imageUploadDetails.assetFile as File).name}` setTokenImageUri(imageUriWithBase) const coverImageUri = await upload( collectionDetails?.imageFile as File[], imageUploadDetails.uploadService, 'cover', imageUploadDetails.nftStorageApiKey as string, imageUploadDetails.pinataApiKey as string, imageUploadDetails.pinataSecretKey as string, ) const coverImageUriWithBase = `ipfs://${coverImageUri}/${(collectionDetails?.imageFile as File[])[0].name}` setCoverImageUrl(coverImageUriWithBase) let thumbnailUri: string | undefined if (imageUploadDetails.isThumbnailCompatible && imageUploadDetails.thumbnailFile) thumbnailUri = await upload( [imageUploadDetails.thumbnailFile] as File[], imageUploadDetails.uploadService, 'thumbnail', imageUploadDetails.nftStorageApiKey as string, imageUploadDetails.pinataApiKey as string, imageUploadDetails.pinataSecretKey as string, ) const thumbnailUriWithBase = thumbnailUri ? `ipfs://${thumbnailUri}/${(imageUploadDetails.thumbnailFile as File).name}` : undefined setThumbnailImageUri(thumbnailUriWithBase) setUploading(false) let whitelist: string | undefined if (whitelistDetails?.whitelistState === 'existing') whitelist = whitelistDetails.contractAddress else if (whitelistDetails?.whitelistState === 'new') whitelist = await instantiateWhitelist() setWhitelistContractAddress(whitelist as string) await instantiateOpenEditionMinter(imageUriWithBase, coverImageUriWithBase, thumbnailUriWithBase, whitelist) } else if (imageUploadDetails?.uploadMethod === 'existing') { setTokenImageUri(imageUploadDetails.imageUrl as string) setCoverImageUrl(imageUploadDetails.coverImageUrl 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 instantiateOpenEditionMinter( imageUploadDetails.imageUrl as string, imageUploadDetails.coverImageUrl as string, whitelist, ) } } setCreationInProgress(false) setReadyToCreate(false) } catch (error: any) { toast.error(error.message, { style: { maxWidth: 'none' }, duration: 10000 }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setReadyToCreate(false) setCreationInProgress(false) setUploading(false) } } const uploadForOffChainStorage = async (): Promise => { if (!offChainMetadataUploadDetails) throw new Error('Please select the asset and fill in the metadata') return new Promise((resolve, reject) => { upload( offChainMetadataUploadDetails.assetFiles, offChainMetadataUploadDetails.uploadService, 'assets', offChainMetadataUploadDetails.nftStorageApiKey as string, offChainMetadataUploadDetails.pinataApiKey as string, offChainMetadataUploadDetails.pinataSecretKey as string, ) .then(async (assetUri: string) => { let thumbnailUri: string | undefined if (offChainMetadataUploadDetails.isThumbnailCompatible && offChainMetadataUploadDetails.thumbnailFile) thumbnailUri = await upload( [offChainMetadataUploadDetails.thumbnailFile] as File[], offChainMetadataUploadDetails.uploadService, 'thumbnail', offChainMetadataUploadDetails.nftStorageApiKey as string, offChainMetadataUploadDetails.pinataApiKey as string, offChainMetadataUploadDetails.pinataSecretKey as string, ) const thumbnailUriWithBase = thumbnailUri ? `ipfs://${thumbnailUri}/${(offChainMetadataUploadDetails.thumbnailFile as File).name}` : undefined const fileArray: File[] = [] const reader: FileReader = new FileReader() reader.onload = (e) => { const data: any = JSON.parse(e.target?.result as string) if (offChainMetadataUploadDetails.isThumbnailCompatible) { data.animation_url = `ipfs://${assetUri}/${offChainMetadataUploadDetails.assetFiles[0].name}` } data.image = offChainMetadataUploadDetails.isThumbnailCompatible && offChainMetadataUploadDetails.thumbnailFile ? thumbnailUriWithBase : `ipfs://${assetUri}/${offChainMetadataUploadDetails.assetFiles[0].name}` if (data.description) { // eslint-disable-next-line @typescript-eslint/no-unsafe-call data.description = data.description.replaceAll('\\n', '\n') } const metadataFileBlob = new Blob([JSON.stringify(data)], { type: 'application/json', }) console.log('Name: ', (offChainMetadataUploadDetails.openEditionMinterMetadataFile as File).name) const updatedMetadataFile = new File( [metadataFileBlob], (offChainMetadataUploadDetails.openEditionMinterMetadataFile as File).name .substring( 0, (offChainMetadataUploadDetails.openEditionMinterMetadataFile as File).name.lastIndexOf('.'), ) .replaceAll('#', ''), { type: 'application/json', }, ) fileArray.push(updatedMetadataFile) } reader.onloadend = () => { upload( fileArray, offChainMetadataUploadDetails.uploadService, 'metadata', offChainMetadataUploadDetails.nftStorageApiKey as string, offChainMetadataUploadDetails.pinataApiKey as string, offChainMetadataUploadDetails.pinataSecretKey as string, ) .then(resolve) .catch(reject) } console.log('File: ', offChainMetadataUploadDetails.openEditionMinterMetadataFile) reader.readAsText(offChainMetadataUploadDetails.openEditionMinterMetadataFile as File, 'utf8') }) .catch(reject) }) } const instantiateWhitelist = async () => { if (!wallet.isWalletConnected) throw new Error('Wallet not connected') if (!whitelistContract) throw new Error('Contract not found') if (whitelistDetails?.whitelistType === 'standard' || whitelistDetails?.whitelistType === 'flex') { const standardMsg = { members: whitelistDetails.members, start_time: whitelistDetails.startTime, end_time: whitelistDetails.endTime, mint_price: coin( String(Number(whitelistDetails.unitPrice)), mintTokenFromFactory ? mintTokenFromFactory.denom : '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)), mintTokenFromFactory ? mintTokenFromFactory.denom : '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 } else if (whitelistDetails?.whitelistType === 'merkletree') { const members = whitelistDetails.members as string[] const membersCsv = members.join('\n') const membersBlob = new Blob([membersCsv], { type: 'text/csv' }) const membersFile = new File([membersBlob], 'members.csv', { type: 'text/csv' }) const formData = new FormData() formData.append('whitelist', membersFile) const response = await toast .promise( axios.post(`${WHITELIST_MERKLE_TREE_API_URL}/create_whitelist`, formData, { headers: { 'Content-Type': 'multipart/form-data', }, }), { loading: 'Fetching merkle root hash...', success: 'Merkle root fetched successfully.', error: 'Error fetching root hash from Whitelist Merkle Tree API.', }, ) .catch((error) => { console.log('error', error) throw new Error('Whitelist instantiation failed.') }) const rootHash = response.data.root_hash console.log('rootHash', rootHash) const merkleTreeMsg = { merkle_root: rootHash, merkle_tree_uri: null, start_time: whitelistDetails.startTime, end_time: whitelistDetails.endTime, mint_price: coin( String(Number(whitelistDetails.unitPrice)), mintTokenFromFactory ? mintTokenFromFactory.denom : 'ustars', ), per_address_limit: whitelistDetails.perAddressLimit, admins: whitelistDetails.admins || [wallet.address], admins_mutable: whitelistDetails.adminsMutable, } const data = await whitelistMerkleTreeContract?.instantiate( WHITELIST_MERKLE_TREE_CODE_ID, merkleTreeMsg, 'Stargaze Whitelist Merkle Tree Contract', wallet.address, ) return data?.contractAddress } } const instantiateOpenEditionMinter = async ( uri: string, coverImageUri: string, thumbnailUri?: string, whitelist?: string, ) => { if (!wallet.isWalletConnected) throw new Error('Wallet not connected') if (!openEditionFactoryContract) throw new Error('Contract not found') if (!openEditionMinterContract) 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: { nft_data: { nft_data_type: metadataStorageMethod === 'off-chain' ? 'off_chain_metadata' : 'on_chain_metadata', token_uri: metadataStorageMethod === 'off-chain' ? uri : null, extension: metadataStorageMethod === 'on-chain' ? { image: imageUploadDetails?.isThumbnailCompatible && imageUploadDetails.thumbnailFile ? thumbnailUri : uri, name: onChainMetadataInputDetails?.name, description: onChainMetadataInputDetails?.description?.replaceAll('\\n', '\n'), attributes: onChainMetadataInputDetails?.attributes, external_url: onChainMetadataInputDetails?.external_url, animation_url: imageUploadDetails?.uploadMethod === 'existing' ? onChainMetadataInputDetails?.animation_url : imageUploadDetails?.isThumbnailCompatible ? uri : undefined, youtube_url: onChainMetadataInputDetails?.youtube_url, } : null, }, start_time: mintingDetails?.startTime, end_time: mintingDetails?.limitType === ('time_limited' as LimitType) || mintingDetails?.limitType === ('time_and_count_limited' as LimitType) ? mintingDetails.endTime : null, mint_price: { amount: Number(mintingDetails?.unitPrice).toString(), denom: (mintTokenFromFactory?.denom as string) || 'ustars', }, per_address_limit: mintingDetails?.perAddressLimit, num_tokens: mintingDetails?.limitType === ('count_limited' as LimitType) || mintingDetails?.limitType === ('time_and_count_limited' as LimitType) ? mintingDetails.tokenCountLimit : null, payment_address: mintingDetails?.paymentAddress || null, whitelist, }, collection_params: { code_id: collectionDetails?.updatable ? SG721_OPEN_EDITION_UPDATABLE_CODE_ID : mintingDetails?.selectedMintToken?.displayName === 'USK' || mintingDetails?.selectedMintToken?.displayName === 'USDC' || mintingDetails?.selectedMintToken?.displayName === 'TIA' || mintingDetails?.selectedMintToken?.displayName === 'STRDST' || mintingDetails?.selectedMintToken?.displayName === 'KUJI' || mintingDetails?.selectedMintToken?.displayName === 'HUAHUA' || mintingDetails?.selectedMintToken?.displayName === 'BRNCH' || mintingDetails?.selectedMintToken?.displayName === 'CRBRUS' ? STRDST_SG721_CODE_ID : SG721_OPEN_EDITION_CODE_ID, name: collectionDetails?.name, symbol: collectionDetails?.symbol, info: { creator: wallet.address, description: collectionDetails?.description.replaceAll('\\n', '\n'), image: coverImageUri, explicit_content: collectionDetails?.explicit || false, royalty_info: royaltyInfo, start_trading_time: collectionDetails?.startTradingTime || null, }, }, }, } const payload: OpenEditionFactoryDispatchExecuteArgs = { contract: openEditionFactoryAddress as string, messages: openEditionFactoryMessages, txSigner: wallet.address || '', msg, funds: [openEditionMinterCreationFee as Coin], updatable: collectionDetails?.updatable, } await openEditionFactoryDispatchExecute(payload) .then((data) => { setTransactionHash(data.transactionHash) setOpenEditionMinterContractAddress(data.openEditionMinterAddress) setSg721ContractAddress(data.sg721Address) }) .catch((error) => { toast.error(error.message, { style: { maxWidth: 'none' } }) addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() }) setUploading(false) setCreationInProgress(false) }) } useEffect(() => { if (minterType !== 'openEdition') { setTransactionHash(null) setOpenEditionMinterContractAddress(null) setSg721ContractAddress(null) setCreationInProgress(false) setUploading(false) } }, [minterType]) useEffect(() => { const data: OpenEditionMinterCreatorDataProps = { metadataStorageMethod, openEditionMinterContractAddress, sg721ContractAddress, whitelistContractAddress, transactionHash, } onChange(data) // eslint-disable-next-line react-hooks/exhaustive-deps }, [ metadataStorageMethod, openEditionMinterContractAddress, sg721ContractAddress, whitelistContractAddress, transactionHash, ]) useEffect(() => { const data: OpenEditionMinterDetailsDataProps = { imageUploadDetails: imageUploadDetails ? imageUploadDetails : undefined, collectionDetails: collectionDetails ? collectionDetails : undefined, whitelistDetails: whitelistDetails ? whitelistDetails : undefined, royaltyDetails: royaltyDetails ? royaltyDetails : undefined, onChainMetadataInputDetails: onChainMetadataInputDetails ? onChainMetadataInputDetails : undefined, offChainMetadataUploadDetails: offChainMetadataUploadDetails ? offChainMetadataUploadDetails : undefined, mintingDetails: mintingDetails ? mintingDetails : undefined, metadataStorageMethod, openEditionMinterContractAddress, coverImageUrl, tokenUri, tokenImageUri, isRefreshed, } onDetailsChange(data) // eslint-disable-next-line react-hooks/exhaustive-deps }, [ imageUploadDetails, collectionDetails, whitelistDetails, royaltyDetails, onChainMetadataInputDetails, offChainMetadataUploadDetails, mintingDetails, metadataStorageMethod, openEditionMinterContractAddress, coverImageUrl, tokenUri, tokenImageUri, isRefreshed, ]) useEffect(() => { if (importedOpenEditionMinterDetails) { setMetadataStorageMethod(importedOpenEditionMinterDetails.metadataStorageMethod as MetadataStorageMethod) } }, [importedOpenEditionMinterDetails]) const fetchWhitelistConfig = async (contractAddress: string | undefined) => { if (contractAddress === '' || !whitelistDetails) return const contract = whitelistContract?.use(contractAddress) await contract ?.config() .then((config) => { if (!config) { whitelistDetails.whitelistType = 'standard' return } if (JSON.stringify(config).includes('whale_cap')) whitelistDetails.whitelistType = 'flex' else if (!JSON.stringify(config).includes('member_limit') || config.member_limit === 0) { // whitelistDetails.whitelistType = 'merkletree' toast.error( 'Whitelist Merkle Tree is not supported yet for open edition collections. Please use a standard or flexible whitelist contract.', ) } else whitelistDetails.whitelistType = 'standard' setIsRefreshed(!isRefreshed) }) .catch((error) => { console.log('error', error) }) } const debouncedWhitelistContractAddress = useDebounce(whitelistDetails?.contractAddress, 300) useEffect(() => { if (whitelistDetails?.whitelistState === 'existing' && debouncedWhitelistContractAddress !== '') { void fetchWhitelistConfig(debouncedWhitelistContractAddress) } }, [whitelistDetails?.whitelistState, debouncedWhitelistContractAddress]) return (
{/* TODO: Cancel once we're able to index on-chain metadata */}
{ setMetadataStorageMethod('off-chain') }} type="radio" value="Off Chain" />
{ setMetadataStorageMethod('on-chain') }} type="radio" value="On Chain" />
) }