Merge pull request #194 from public-awesome/export-import-collection-config

Export/Import collection creation configuration
This commit is contained in:
Jorge Hernandez 2023-08-30 07:52:48 -06:00 committed by GitHub
commit 1a9d7ac9c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 487 additions and 49 deletions

View File

@ -1,4 +1,4 @@
APP_VERSION=0.7.3
APP_VERSION=0.7.4
NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
NEXT_PUBLIC_SG721_CODE_ID=2595

View File

@ -13,6 +13,8 @@ export interface MetadataInputProps {
selectedAssetFile: File
selectedMetadataFile: File
updateMetadataToUpload: (metadataFile: File) => void
onChange?: (metadata: any) => void
importedMetadata?: any
}
export const MetadataInput = (props: MetadataInputProps) => {
@ -151,9 +153,12 @@ export const MetadataInput = (props: MetadataInputProps) => {
useEffect(() => {
console.log(props.selectedMetadataFile?.name)
if (props.selectedMetadataFile) void parseMetadata(props.selectedMetadataFile)
else void parseMetadata(emptyMetadataFile)
}, [props.selectedMetadataFile?.name])
if (props.selectedMetadataFile) {
void parseMetadata(props.selectedMetadataFile)
} else if (!props.importedMetadata) {
void parseMetadata(emptyMetadataFile)
}
}, [props.selectedMetadataFile?.name, props.importedMetadata])
const nameStateMemo = useMemo(() => nameState, [nameState.value])
const descriptionStateMemo = useMemo(() => descriptionState, [descriptionState.value])
@ -163,7 +168,10 @@ export const MetadataInput = (props: MetadataInputProps) => {
useEffect(() => {
console.log('Update metadata')
if (metadata) generateUpdatedMetadata()
if (metadata) {
generateUpdatedMetadata()
if (props.onChange) props.onChange(metadata)
}
console.log(metadata)
}, [
nameStateMemo.value,
@ -173,6 +181,33 @@ export const MetadataInput = (props: MetadataInputProps) => {
attributesStateMemo.entries,
])
useEffect(() => {
if (props.importedMetadata) {
void parseMetadata(emptyMetadataFile).then(() => {
console.log('Imported metadata: ', props.importedMetadata)
nameState.onChange(props.importedMetadata.name || '')
descriptionState.onChange(props.importedMetadata.description || '')
externalUrlState.onChange(props.importedMetadata.external_url || '')
youtubeUrlState.onChange(props.importedMetadata.youtube_url || '')
if (props.importedMetadata?.attributes && props.importedMetadata?.attributes?.length > 0) {
attributesState.reset()
props.importedMetadata?.attributes?.forEach((attribute: { trait_type: string; value: string }) => {
attributesState.add({
trait_type: attribute.trait_type,
value: attribute.value,
})
})
} else {
attributesState.reset()
attributesState.add({
trait_type: '',
value: '',
})
}
})
}
}, [props.importedMetadata])
return (
<div>
<div className="grid grid-cols-2 mt-4 mr-4 ml-8 w-full max-w-6xl max-h-full no-scrollbar">

View File

@ -28,6 +28,7 @@ export interface MinterInfo {
interface BaseMinterDetailsProps {
onChange: (data: BaseMinterDetailsDataProps) => void
minterType: MinterType
importedBaseMinterDetails?: BaseMinterDetailsDataProps
}
export interface BaseMinterDetailsDataProps {
@ -37,7 +38,7 @@ export interface BaseMinterDetailsDataProps {
collectionTokenCount: number | undefined
}
export const BaseMinterDetails = ({ onChange, minterType }: BaseMinterDetailsProps) => {
export const BaseMinterDetails = ({ onChange, minterType, importedBaseMinterDetails }: BaseMinterDetailsProps) => {
const wallet = useWallet()
const [myBaseMinterContracts, setMyBaseMinterContracts] = useState<MinterInfo[]>([])
@ -198,6 +199,15 @@ export const BaseMinterDetails = ({ onChange, minterType }: BaseMinterDetailsPro
collectionTokenCount,
])
useEffect(() => {
if (importedBaseMinterDetails) {
setBaseMinterAcquisitionMethod(importedBaseMinterDetails.baseMinterAcquisitionMethod)
existingBaseMinterState.onChange(
importedBaseMinterDetails.existingBaseMinter ? importedBaseMinterDetails.existingBaseMinter : '',
)
}
}, [importedBaseMinterDetails])
return (
<div className="mx-10 mb-4 rounded border-2 border-white/20">
<div className="flex justify-center mb-2">

View File

@ -26,6 +26,7 @@ interface CollectionDetailsProps {
uploadMethod: UploadMethod
coverImageUrl: string
minterType: MinterType
importedCollectionDetails?: CollectionDetailsDataProps
}
export interface CollectionDetailsDataProps {
@ -39,7 +40,13 @@ export interface CollectionDetailsDataProps {
updatable: boolean
}
export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl, minterType }: CollectionDetailsProps) => {
export const CollectionDetails = ({
onChange,
uploadMethod,
coverImageUrl,
minterType,
importedCollectionDetails,
}: CollectionDetailsProps) => {
const [coverImage, setCoverImage] = useState<File | null>(null)
const [timestamp, setTimestamp] = useState<Date | undefined>()
const [explicit, setExplicit] = useState<boolean>(false)
@ -105,6 +112,23 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl, minte
updatable,
])
useEffect(() => {
if (importedCollectionDetails) {
nameState.onChange(importedCollectionDetails.name)
descriptionState.onChange(importedCollectionDetails.description)
symbolState.onChange(importedCollectionDetails.symbol)
externalLinkState.onChange(importedCollectionDetails.externalLink || '')
setTimestamp(
importedCollectionDetails.startTradingTime
? new Date(parseInt(importedCollectionDetails.startTradingTime) / 1_000_000)
: undefined,
)
setExplicit(importedCollectionDetails.explicit)
setUpdatable(importedCollectionDetails.updatable)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importedCollectionDetails])
const selectCoverImage = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files === null) return toast.error('Error selecting cover image')
if (event.target.files.length === 0) {

View File

@ -20,6 +20,7 @@ interface MintingDetailsProps {
uploadMethod: UploadMethod
minimumMintPrice: number
mintingTokenFromFactory?: TokenInfo
importedMintingDetails?: MintingDetailsDataProps
}
export interface MintingDetailsDataProps {
@ -37,6 +38,7 @@ export const MintingDetails = ({
uploadMethod,
minimumMintPrice,
mintingTokenFromFactory,
importedMintingDetails,
}: MintingDetailsProps) => {
const wallet = useWallet()
@ -113,6 +115,18 @@ export const MintingDetails = ({
selectedMintToken,
])
useEffect(() => {
if (importedMintingDetails) {
numberOfTokensState.onChange(importedMintingDetails.numTokens)
unitPriceState.onChange(Number(importedMintingDetails.unitPrice) / 1_000_000)
perAddressLimitState.onChange(importedMintingDetails.perAddressLimit)
setTimestamp(new Date(Number(importedMintingDetails.startTime) / 1_000_000))
paymentAddressState.onChange(importedMintingDetails.paymentAddress ? importedMintingDetails.paymentAddress : '')
setSelectedMintToken(tokensList.find((token) => token.id === importedMintingDetails.selectedMintToken?.id))
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importedMintingDetails])
return (
<div>
<FormGroup subtitle="Information about your minting settings" title="Minting Details">
@ -127,6 +141,7 @@ export const MintingDetails = ({
<select
className="py-[9px] px-4 mt-14 ml-2 placeholder:text-white/50 bg-white/10 rounded border-2 border-white/20 focus:ring focus:ring-plumbus-20"
onChange={(e) => setSelectedMintToken(tokensList.find((t) => t.displayName === e.target.value))}
value={selectedMintToken?.displayName}
>
{vendingMinterList
.filter((minter) => minter.factoryAddress !== undefined && minter.updatable === false)

View File

@ -9,6 +9,7 @@ import { NumberInput, TextInput } from '../../forms/FormInput'
interface RoyaltyDetailsProps {
onChange: (data: RoyaltyDetailsDataProps) => void
importedRoyaltyDetails?: RoyaltyDetailsDataProps
}
export interface RoyaltyDetailsDataProps {
@ -19,7 +20,7 @@ export interface RoyaltyDetailsDataProps {
type RoyaltyState = 'none' | 'new'
export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
export const RoyaltyDetails = ({ onChange, importedRoyaltyDetails }: RoyaltyDetailsProps) => {
const wallet = useWallet()
const [royaltyState, setRoyaltyState] = useState<RoyaltyState>('none')
@ -60,6 +61,15 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [royaltyState, royaltyPaymentAddressState.value, royaltyShareState.value])
useEffect(() => {
if (importedRoyaltyDetails) {
setRoyaltyState(importedRoyaltyDetails.royaltyType)
royaltyPaymentAddressState.onChange(importedRoyaltyDetails.paymentAddress)
royaltyShareState.onChange(importedRoyaltyDetails.share.toString())
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importedRoyaltyDetails])
return (
<div className="py-3 px-8 rounded border-2 border-white/20">
<div className="flex justify-center">

View File

@ -31,6 +31,7 @@ interface UploadDetailsProps {
onChange: (value: UploadDetailsDataProps) => void
minterType: MinterType
baseMinterAcquisitionMethod?: BaseMinterAcquisitionMethod
importedUploadDetails?: UploadDetailsDataProps
}
export interface UploadDetailsDataProps {
@ -46,7 +47,12 @@ export interface UploadDetailsDataProps {
baseMinterMetadataFile?: File
}
export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMethod }: UploadDetailsProps) => {
export const UploadDetails = ({
onChange,
minterType,
baseMinterAcquisitionMethod,
importedUploadDetails,
}: UploadDetailsProps) => {
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
@ -274,10 +280,31 @@ export const UploadDetails = ({ onChange, minterType, baseMinterAcquisitionMetho
setMetadataFilesArray([])
if (assetFilesRef.current) assetFilesRef.current.value = ''
setAssetFilesArray([])
if (!importedUploadDetails || minterType === 'base') {
baseTokenUriState.onChange('')
coverImageUrlState.onChange('')
}
}, [uploadMethod, minterType, baseMinterAcquisitionMethod])
useEffect(() => {
if (importedUploadDetails) {
if (importedUploadDetails.uploadMethod === 'new') {
setUploadMethod('new')
setUploadService(importedUploadDetails.uploadService)
nftStorageApiKeyState.onChange(importedUploadDetails.nftStorageApiKey || '')
pinataApiKeyState.onChange(importedUploadDetails.pinataApiKey || '')
pinataSecretKeyState.onChange(importedUploadDetails.pinataSecretKey || '')
baseTokenUriState.onChange(importedUploadDetails.baseTokenURI || '')
coverImageUrlState.onChange(importedUploadDetails.imageUrl || '')
} else if (importedUploadDetails.uploadMethod === 'existing') {
setUploadMethod('existing')
baseTokenUriState.onChange(importedUploadDetails.baseTokenURI || '')
coverImageUrlState.onChange(importedUploadDetails.imageUrl || '')
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importedUploadDetails])
return (
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
<div className="flex justify-center">

View File

@ -9,6 +9,7 @@ import { InputDateTime } from 'components/InputDateTime'
import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload'
import { WhitelistFlexUpload } from 'components/WhitelistFlexUpload'
import type { TokenInfo } from 'config/token'
import { useWallet } from 'contexts/wallet'
import React, { useEffect, useState } from 'react'
import { isValidAddress } from 'utils/isValidAddress'
@ -20,6 +21,7 @@ import { WhitelistUpload } from '../../WhitelistUpload'
interface WhitelistDetailsProps {
onChange: (data: WhitelistDetailsDataProps) => void
mintingTokenFromFactory?: TokenInfo
importedWhitelistDetails?: WhitelistDetailsDataProps
}
export interface WhitelistDetailsDataProps {
@ -40,7 +42,13 @@ type WhitelistState = 'none' | 'existing' | 'new'
type WhitelistType = 'standard' | 'flex'
export const WhitelistDetails = ({ onChange, mintingTokenFromFactory }: WhitelistDetailsProps) => {
export const WhitelistDetails = ({
onChange,
mintingTokenFromFactory,
importedWhitelistDetails,
}: WhitelistDetailsProps) => {
const wallet = useWallet()
const [whitelistState, setWhitelistState] = useState<WhitelistState>('none')
const [whitelistType, setWhitelistType] = useState<WhitelistType>('standard')
const [startDate, setStartDate] = useState<Date | undefined>(undefined)
@ -93,8 +101,10 @@ export const WhitelistDetails = ({ onChange, mintingTokenFromFactory }: Whitelis
}
useEffect(() => {
if (!importedWhitelistDetails) {
setWhitelistStandardArray([])
setWhitelistFlexArray([])
}
}, [whitelistType])
useEffect(() => {
@ -142,6 +152,63 @@ export const WhitelistDetails = ({ onChange, mintingTokenFromFactory }: Whitelis
adminsMutable,
])
// make the necessary changes with respect to imported whitelist details
useEffect(() => {
if (importedWhitelistDetails) {
setWhitelistState(importedWhitelistDetails.whitelistState)
setWhitelistType(importedWhitelistDetails.whitelistType)
whitelistAddressState.onChange(
importedWhitelistDetails.contractAddress ? importedWhitelistDetails.contractAddress : '',
)
unitPriceState.onChange(
importedWhitelistDetails.unitPrice ? Number(importedWhitelistDetails.unitPrice) / 1000000 : 0,
)
memberLimitState.onChange(importedWhitelistDetails.memberLimit ? importedWhitelistDetails.memberLimit : 0)
perAddressLimitState.onChange(
importedWhitelistDetails.perAddressLimit ? importedWhitelistDetails.perAddressLimit : 0,
)
setStartDate(
importedWhitelistDetails.startTime
? new Date(Number(importedWhitelistDetails.startTime) / 1_000_000)
: undefined,
)
setEndDate(
importedWhitelistDetails.endTime ? new Date(Number(importedWhitelistDetails.endTime) / 1_000_000) : undefined,
)
setAdminsMutable(importedWhitelistDetails.adminsMutable ? importedWhitelistDetails.adminsMutable : true)
importedWhitelistDetails.admins?.forEach((admin) => {
addressListState.reset()
addressListState.add({ address: admin })
})
if (importedWhitelistDetails.whitelistType === 'standard') {
setWhitelistStandardArray([])
importedWhitelistDetails.members?.forEach((member) => {
setWhitelistStandardArray((standardArray) => [...standardArray, member as string])
})
} else {
setWhitelistFlexArray([])
importedWhitelistDetails.members?.forEach((member) => {
setWhitelistFlexArray((flexArray) => [
...flexArray,
{
address: (member as WhitelistFlexMember).address,
mint_count: (member as WhitelistFlexMember).mint_count,
},
])
})
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importedWhitelistDetails])
useEffect(() => {
if (whitelistState === 'new' && wallet.address) {
addressListState.reset()
addressListState.add({ address: wallet.address })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [whitelistState, wallet.address])
return (
<div className="py-3 px-8 rounded border-2 border-white/20">
<div className="flex justify-center">
@ -290,7 +357,6 @@ export const WhitelistDetails = ({ onChange, mintingTokenFromFactory }: Whitelis
<div className="my-4 ml-4">
<AddressList
entries={addressListState.entries}
isRequired
onAdd={addressListState.add}
onChange={addressListState.update}
onRemove={addressListState.remove}

View File

@ -31,6 +31,7 @@ export function AddressList(props: AddressListProps) {
{entries.map(([id], i) => (
<Address
key={`ib-${id}`}
defaultValue={entries[i][1]}
id={id}
isLast={i === entries.length - 1}
onAdd={onAdd}
@ -48,9 +49,10 @@ export interface AddressProps {
onAdd: AddressListProps['onAdd']
onChange: AddressListProps['onChange']
onRemove: AddressListProps['onRemove']
defaultValue?: Address
}
export function Address({ id, isLast, onAdd, onChange, onRemove }: AddressProps) {
export function Address({ id, isLast, onAdd, onChange, onRemove, defaultValue }: AddressProps) {
const wallet = useWallet()
const Icon = useMemo(() => (isLast ? FaPlus : FaMinus), [isLast])
@ -60,6 +62,7 @@ export function Address({ id, isLast, onAdd, onChange, onRemove }: AddressProps)
id: `ib-address-${htmlId}`,
name: `ib-address-${htmlId}`,
title: ``,
defaultValue: defaultValue?.address,
})
const resolveAddress = async (name: string) => {

View File

@ -28,6 +28,7 @@ interface CollectionDetailsProps {
uploadMethod: UploadMethod
coverImageUrl: string
metadataStorageMethod: MetadataStorageMethod
importedCollectionDetails?: CollectionDetailsDataProps
}
export interface CollectionDetailsDataProps {
@ -46,6 +47,7 @@ export const CollectionDetails = ({
uploadMethod,
metadataStorageMethod,
coverImageUrl,
importedCollectionDetails,
}: CollectionDetailsProps) => {
const [coverImage, setCoverImage] = useState<File | null>(null)
const [timestamp, setTimestamp] = useState<Date | undefined>()
@ -152,6 +154,23 @@ export const CollectionDetails = ({
}
}, [updatable])
useEffect(() => {
if (importedCollectionDetails) {
nameState.onChange(importedCollectionDetails.name)
descriptionState.onChange(importedCollectionDetails.description)
symbolState.onChange(importedCollectionDetails.symbol)
//setCoverImage(importedCollectionDetails.imageFile[0] || null)
externalLinkState.onChange(importedCollectionDetails.externalLink || '')
setTimestamp(
importedCollectionDetails.startTradingTime
? new Date(parseInt(importedCollectionDetails.startTradingTime) / 1_000_000)
: undefined,
)
setExplicit(importedCollectionDetails.explicit)
setUpdatable(importedCollectionDetails.updatable)
}
}, [importedCollectionDetails])
const videoPreview = useMemo(() => {
if (uploadMethod === 'new' && coverImage) {
return (

View File

@ -20,6 +20,7 @@ export type UploadMethod = 'new' | 'existing'
interface ImageUploadDetailsProps {
onChange: (value: ImageUploadDetailsDataProps) => void
importedImageUploadDetails?: ImageUploadDetailsDataProps
}
export interface ImageUploadDetailsDataProps {
@ -33,7 +34,7 @@ export interface ImageUploadDetailsDataProps {
coverImageUrl?: string
}
export const ImageUploadDetails = ({ onChange }: ImageUploadDetailsProps) => {
export const ImageUploadDetails = ({ onChange, importedImageUploadDetails }: ImageUploadDetailsProps) => {
const [assetFile, setAssetFile] = useState<File>()
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
@ -140,6 +141,18 @@ export const ImageUploadDetails = ({ onChange }: ImageUploadDetailsProps) => {
imageUrlState.onChange('')
}, [uploadMethod])
useEffect(() => {
if (importedImageUploadDetails) {
setUploadMethod(importedImageUploadDetails.uploadMethod)
setUploadService(importedImageUploadDetails.uploadService)
nftStorageApiKeyState.onChange(importedImageUploadDetails.nftStorageApiKey || '')
pinataApiKeyState.onChange(importedImageUploadDetails.pinataApiKey || '')
pinataSecretKeyState.onChange(importedImageUploadDetails.pinataSecretKey || '')
imageUrlState.onChange(importedImageUploadDetails.imageUrl || '')
coverImageUrlState.onChange(importedImageUploadDetails.coverImageUrl || '')
}
}, [importedImageUploadDetails])
const previewUrl = imageUrlState.value.toLowerCase().trim().startsWith('ipfs://')
? `https://ipfs-gw.stargaze-apis.com/ipfs/${imageUrlState.value.substring(7)}`
: imageUrlState.value

View File

@ -19,6 +19,7 @@ interface MintingDetailsProps {
uploadMethod: UploadMethod
minimumMintPrice: number
mintTokenFromFactory?: TokenInfo | undefined
importedMintingDetails?: MintingDetailsDataProps
}
export interface MintingDetailsDataProps {
@ -35,12 +36,14 @@ export const MintingDetails = ({
uploadMethod,
minimumMintPrice,
mintTokenFromFactory,
importedMintingDetails,
}: MintingDetailsProps) => {
const wallet = useWallet()
const [timestamp, setTimestamp] = useState<Date | undefined>()
const [endTimestamp, setEndTimestamp] = useState<Date | undefined>()
const [selectedMintToken, setSelectedMintToken] = useState<TokenInfo | undefined>(stars)
const [mintingDetailsImported, setMintingDetailsImported] = useState(false)
const unitPriceState = useNumberInputState({
id: 'unitPrice',
@ -75,7 +78,9 @@ export const MintingDetails = ({
}
useEffect(() => {
if (!importedMintingDetails || (importedMintingDetails && mintingDetailsImported)) {
void resolvePaymentAddress()
}
}, [paymentAddressState.value])
useEffect(() => {
@ -102,6 +107,20 @@ export const MintingDetails = ({
selectedMintToken,
])
useEffect(() => {
if (importedMintingDetails) {
console.log('Selected Token ID: ', importedMintingDetails.selectedMintToken?.id)
unitPriceState.onChange(Number(importedMintingDetails.unitPrice) / 1000000)
perAddressLimitState.onChange(importedMintingDetails.perAddressLimit)
setTimestamp(new Date(Number(importedMintingDetails.startTime) / 1_000_000))
setEndTimestamp(new Date(Number(importedMintingDetails.endTime) / 1_000_000))
paymentAddressState.onChange(importedMintingDetails.paymentAddress ? importedMintingDetails.paymentAddress : '')
setSelectedMintToken(tokensList.find((token) => token.id === importedMintingDetails.selectedMintToken?.id))
setMintingDetailsImported(true)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importedMintingDetails])
return (
<div className="border-l-[1px] border-gray-500 border-opacity-20">
<FormGroup subtitle="Information about your minting settings" title="Minting Details">
@ -110,6 +129,7 @@ export const MintingDetails = ({
<select
className="py-[9px] px-4 mt-14 ml-4 placeholder:text-white/50 bg-white/10 rounded border-2 border-white/20 focus:ring focus:ring-plumbus-20"
onChange={(e) => setSelectedMintToken(tokensList.find((t) => t.displayName === e.target.value))}
value={selectedMintToken?.displayName}
>
{openEditionMinterList
.filter((minter) => minter.factoryAddress !== undefined && minter.updatable === false)

View File

@ -28,6 +28,7 @@ export type UploadMethod = 'new' | 'existing'
interface OffChainMetadataUploadDetailsProps {
onChange: (value: OffChainMetadataUploadDetailsDataProps) => void
metadataStorageMethod?: MetadataStorageMethod
importedOffChainMetadataUploadDetails?: OffChainMetadataUploadDetailsDataProps
}
export interface OffChainMetadataUploadDetailsDataProps {
@ -41,11 +42,13 @@ export interface OffChainMetadataUploadDetailsDataProps {
tokenURI?: string
imageUrl?: string
openEditionMinterMetadataFile?: File
exportedMetadata?: any
}
export const OffChainMetadataUploadDetails = ({
onChange,
metadataStorageMethod,
importedOffChainMetadataUploadDetails,
}: OffChainMetadataUploadDetailsProps) => {
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
@ -53,6 +56,7 @@ export const OffChainMetadataUploadDetails = ({
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0)
const [refreshMetadata, setRefreshMetadata] = useState(false)
const [exportedMetadata, setExportedMetadata] = useState(undefined)
const [openEditionMinterMetadataFile, setOpenEditionMinterMetadataFile] = useState<File | undefined>()
@ -204,6 +208,7 @@ export const OffChainMetadataUploadDetails = ({
.replace(regex, '')
.trim(),
openEditionMinterMetadataFile,
exportedMetadata,
}
onChange(data)
} catch (error: any) {
@ -222,6 +227,7 @@ export const OffChainMetadataUploadDetails = ({
coverImageUrlState.value,
refreshMetadata,
openEditionMinterMetadataFile,
exportedMetadata,
])
useEffect(() => {
@ -229,10 +235,25 @@ export const OffChainMetadataUploadDetails = ({
setMetadataFilesArray([])
if (assetFilesRef.current) assetFilesRef.current.value = ''
setAssetFilesArray([])
if (!importedOffChainMetadataUploadDetails) {
tokenUriState.onChange('')
coverImageUrlState.onChange('')
}
}, [uploadMethod, metadataStorageMethod])
useEffect(() => {
if (importedOffChainMetadataUploadDetails) {
setUploadService(importedOffChainMetadataUploadDetails.uploadService)
nftStorageApiKeyState.onChange(importedOffChainMetadataUploadDetails.nftStorageApiKey || '')
pinataApiKeyState.onChange(importedOffChainMetadataUploadDetails.pinataApiKey || '')
pinataSecretKeyState.onChange(importedOffChainMetadataUploadDetails.pinataSecretKey || '')
setUploadMethod(importedOffChainMetadataUploadDetails.uploadMethod)
tokenUriState.onChange(importedOffChainMetadataUploadDetails.tokenURI || '')
coverImageUrlState.onChange(importedOffChainMetadataUploadDetails.imageUrl || '')
// setOpenEditionMinterMetadataFile(importedOffChainMetadataUploadDetails.openEditionMinterMetadataFile)
}
}, [importedOffChainMetadataUploadDetails])
return (
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
<div className="flex justify-center">
@ -447,6 +468,8 @@ export const OffChainMetadataUploadDetails = ({
/>
</div>
<MetadataInput
importedMetadata={importedOffChainMetadataUploadDetails?.exportedMetadata}
onChange={setExportedMetadata}
selectedAssetFile={assetFilesArray[0]}
selectedMetadataFile={metadataFilesArray[0]}
updateMetadataToUpload={updateOpenEditionMinterMetadataFile}

View File

@ -21,6 +21,7 @@ import type { UploadMethod } from './ImageUploadDetails'
interface OnChainMetadataInputDetailsProps {
onChange: (data: OnChainMetadataInputDetailsDataProps) => void
uploadMethod: UploadMethod | undefined
importedOnChainMetadataInputDetails?: OnChainMetadataInputDetailsDataProps
}
export interface OnChainMetadataInputDetailsDataProps {
@ -34,7 +35,11 @@ export interface OnChainMetadataInputDetailsDataProps {
youtube_url?: string
}
export const OnChainMetadataInputDetails = ({ onChange, uploadMethod }: OnChainMetadataInputDetailsProps) => {
export const OnChainMetadataInputDetails = ({
onChange,
uploadMethod,
importedOnChainMetadataInputDetails,
}: OnChainMetadataInputDetailsProps) => {
const wallet = useWallet()
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
const [metadataFile, setMetadataFile] = useState<File>()
@ -196,6 +201,26 @@ export const OnChainMetadataInputDetails = ({ onChange, uploadMethod }: OnChainM
youtubeUrlState.value,
])
useEffect(() => {
if (importedOnChainMetadataInputDetails) {
nameState.onChange(importedOnChainMetadataInputDetails.name || '')
descriptionState.onChange(importedOnChainMetadataInputDetails.description || '')
externalUrlState.onChange(importedOnChainMetadataInputDetails.external_url || '')
youtubeUrlState.onChange(importedOnChainMetadataInputDetails.youtube_url || '')
animationUrlState.onChange(importedOnChainMetadataInputDetails.animation_url || '')
imageDataState.onChange(importedOnChainMetadataInputDetails.image_data || '')
if (importedOnChainMetadataInputDetails.attributes) {
attributesState.reset()
importedOnChainMetadataInputDetails.attributes.forEach((attr) => {
attributesState.add({
trait_type: attr.trait_type,
value: attr.value,
})
})
}
}
}, [importedOnChainMetadataInputDetails])
return (
<div className="py-3 px-8 rounded border-2 border-white/20">
<span className="ml-4 text-xl font-bold underline underline-offset-4">NFT Metadata</span>

View File

@ -57,6 +57,10 @@ export interface OpenEditionMinterDetailsDataProps {
offChainMetadataUploadDetails?: OffChainMetadataUploadDetailsDataProps
mintingDetails?: MintingDetailsDataProps
metadataStorageMethod?: MetadataStorageMethod
openEditionMinterContractAddress?: string | null
coverImageUrl?: string | null
tokenUri?: string | null
tokenImageUri?: string | null
}
interface OpenEditionMinterCreatorProps {
@ -68,6 +72,7 @@ interface OpenEditionMinterCreatorProps {
minimumUpdatableMintPrice?: string
minterType?: MinterType
mintTokenFromFactory?: TokenInfo | undefined
importedOpenEditionMinterDetails?: OpenEditionMinterDetailsDataProps
}
export interface OpenEditionMinterCreatorDataProps {
@ -86,6 +91,7 @@ export const OpenEditionMinterCreator = ({
minimumUpdatableMintPrice,
minterType,
mintTokenFromFactory,
importedOpenEditionMinterDetails,
}: OpenEditionMinterCreatorProps) => {
const wallet = useWallet()
const { openEditionMinter: openEditionMinterContract, openEditionFactory: openEditionFactoryContract } =
@ -303,7 +309,13 @@ export const OpenEditionMinterCreator = ({
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.endTime === '') throw new Error('End time is required')
if (Number(mintingDetails.startTime) < new Date().getTime() * 1000000) throw new Error('Invalid start time')
if (Number(mintingDetails.endTime) < Number(mintingDetails.startTime))
throw new Error('End time cannot be earlier than start time')
if (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'))
@ -577,8 +589,6 @@ export const OpenEditionMinterCreator = ({
},
}
console.log('msg: ', msg)
console.log('Using factory address: ', factoryAddressForSelectedDenom)
const payload: OpenEditionFactoryDispatchExecuteArgs = {
contract: collectionDetails?.updatable ? updatableFactoryAddressForSelectedDenom : factoryAddressForSelectedDenom,
messages: openEditionFactoryMessages,
@ -637,6 +647,10 @@ export const OpenEditionMinterCreator = ({
offChainMetadataUploadDetails: offChainMetadataUploadDetails ? offChainMetadataUploadDetails : undefined,
mintingDetails: mintingDetails ? mintingDetails : undefined,
metadataStorageMethod,
openEditionMinterContractAddress,
coverImageUrl,
tokenUri,
tokenImageUri,
}
onDetailsChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -647,8 +661,19 @@ export const OpenEditionMinterCreator = ({
onChainMetadataInputDetails,
offChainMetadataUploadDetails,
mintingDetails,
metadataStorageMethod,
openEditionMinterContractAddress,
coverImageUrl,
tokenUri,
tokenImageUri,
])
useEffect(() => {
if (importedOpenEditionMinterDetails) {
setMetadataStorageMethod(importedOpenEditionMinterDetails.metadataStorageMethod as MetadataStorageMethod)
}
}, [importedOpenEditionMinterDetails])
return (
<div>
{/* TODO: Cancel once we're able to index on-chain metadata */}
@ -696,16 +721,23 @@ export const OpenEditionMinterCreator = ({
</div>
</div>
</Conditional>
<div className={clsx('my-4 mx-10')}>
<div className={clsx('my-0 mx-10')}>
<Conditional test={metadataStorageMethod === 'off-chain'}>
<div>
<OffChainMetadataUploadDetails onChange={setOffChainMetadataUploadDetails} />
<OffChainMetadataUploadDetails
importedOffChainMetadataUploadDetails={importedOpenEditionMinterDetails?.offChainMetadataUploadDetails}
onChange={setOffChainMetadataUploadDetails}
/>
</div>
</Conditional>
<Conditional test={metadataStorageMethod === 'on-chain'}>
<div>
<ImageUploadDetails onChange={setImageUploadDetails} />
<ImageUploadDetails
importedImageUploadDetails={importedOpenEditionMinterDetails?.imageUploadDetails}
onChange={setImageUploadDetails}
/>
<OnChainMetadataInputDetails
importedOnChainMetadataInputDetails={importedOpenEditionMinterDetails?.onChainMetadataInputDetails}
onChange={setOnChainMetadataInputDetails}
uploadMethod={imageUploadDetails?.uploadMethod}
/>
@ -719,6 +751,7 @@ export const OpenEditionMinterCreator = ({
? (offChainMetadataUploadDetails?.imageUrl as string)
: (imageUploadDetails?.coverImageUrl as string)
}
importedCollectionDetails={importedOpenEditionMinterDetails?.collectionDetails}
metadataStorageMethod={metadataStorageMethod}
onChange={setCollectionDetails}
uploadMethod={
@ -728,6 +761,7 @@ export const OpenEditionMinterCreator = ({
}
/>
<MintingDetails
importedMintingDetails={importedOpenEditionMinterDetails?.mintingDetails}
minimumMintPrice={
collectionDetails?.updatable
? Number(minimumUpdatableMintPrice) / 1000000
@ -739,7 +773,10 @@ export const OpenEditionMinterCreator = ({
/>
</div>
<div className="my-6">
<RoyaltyDetails onChange={setRoyaltyDetails} />
<RoyaltyDetails
importedRoyaltyDetails={importedOpenEditionMinterDetails?.royaltyDetails}
onChange={setRoyaltyDetails}
/>
</div>
<div className="flex justify-end w-full">
<Button

View File

@ -9,6 +9,7 @@ import { NumberInput, TextInput } from '../forms/FormInput'
interface RoyaltyDetailsProps {
onChange: (data: RoyaltyDetailsDataProps) => void
importedRoyaltyDetails?: RoyaltyDetailsDataProps
}
export interface RoyaltyDetailsDataProps {
@ -19,9 +20,10 @@ export interface RoyaltyDetailsDataProps {
type RoyaltyState = 'none' | 'new'
export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
export const RoyaltyDetails = ({ onChange, importedRoyaltyDetails }: RoyaltyDetailsProps) => {
const wallet = useWallet()
const [royaltyState, setRoyaltyState] = useState<RoyaltyState>('none')
const [royaltyDetailsImported, setRoyaltyDetailsImported] = useState(false)
const royaltyPaymentAddressState = useInputState({
id: 'royalty-payment-address',
@ -40,6 +42,7 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
})
useEffect(() => {
if (!importedRoyaltyDetails || (importedRoyaltyDetails && royaltyDetailsImported)) {
void resolveAddress(
royaltyPaymentAddressState.value
.toLowerCase()
@ -57,9 +60,20 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
}
onChange(data)
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [royaltyState, royaltyPaymentAddressState.value, royaltyShareState.value])
useEffect(() => {
if (importedRoyaltyDetails) {
setRoyaltyState(importedRoyaltyDetails.royaltyType)
royaltyPaymentAddressState.onChange(importedRoyaltyDetails.paymentAddress.toString())
royaltyShareState.onChange(importedRoyaltyDetails.share.toString())
setRoyaltyDetailsImported(true)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importedRoyaltyDetails])
return (
<div className="py-3 px-8 mx-10 rounded border-2 border-white/20">
<div className="flex justify-center">

View File

@ -1,6 +1,6 @@
{
"name": "stargaze-studio",
"version": "0.7.3",
"version": "0.7.4",
"workspaces": [
"packages/*"
],

View File

@ -30,6 +30,7 @@ import type { RoyaltyDetailsDataProps } from 'components/collections/creation/Ro
import type { UploadDetailsDataProps } from 'components/collections/creation/UploadDetails'
import type { WhitelistDetailsDataProps } from 'components/collections/creation/WhitelistDetails'
import { Conditional } from 'components/Conditional'
import { FormControl } from 'components/FormControl'
import { LoadingModal } from 'components/LoadingModal'
import type { OpenEditionMinterCreatorDataProps } from 'components/openEdition/OpenEditionMinterCreator'
import { OpenEditionMinterCreator } from 'components/openEdition/OpenEditionMinterCreator'
@ -44,6 +45,7 @@ import type { DispatchExecuteArgs as VendingFactoryDispatchExecuteArgs } from 'c
import { dispatchExecute as vendingFactoryDispatchExecute } from 'contracts/vendingFactory/messages/execute'
import type { NextPage } from 'next'
import { NextSeo } from 'next-seo'
import type { ChangeEvent } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import { upload } from 'services/upload'
@ -90,6 +92,17 @@ const CollectionCreationPage: NextPage = () => {
const scrollRef = useRef<HTMLDivElement>(null)
const sidetabRef = useRef<any>(null)
const [importedDetails, setImportedDetails] = useState<{
minterType: MinterType
collectionDetails: CollectionDetailsDataProps
uploadDetails: UploadDetailsDataProps
mintingDetails: MintingDetailsDataProps
whitelistDetails: WhitelistDetailsDataProps
royaltyDetails: RoyaltyDetailsDataProps
baseMinterDetails: BaseMinterDetailsDataProps
openEditionMinterDetails: OpenEditionMinterDetailsDataProps
}>()
const [uploadDetails, setUploadDetails] = useState<UploadDetailsDataProps | null>(null)
const [collectionDetails, setCollectionDetails] = useState<CollectionDetailsDataProps | null>(null)
const [baseMinterDetails, setBaseMinterDetails] = useState<BaseMinterDetailsDataProps | null>(null)
@ -1277,6 +1290,64 @@ const CollectionCreationPage: NextPage = () => {
})
}
const exportDetails = () => {
const details = {
minterType,
collectionDetails,
uploadDetails,
mintingDetails,
whitelistDetails,
royaltyDetails,
baseMinterDetails,
openEditionMinterDetails,
vendingMinterContractAddress,
baseTokenUri: `${baseTokenUri?.startsWith('ipfs://') ? baseTokenUri : `ipfs://${baseTokenUri}`}`,
coverImageUrl:
uploadDetails?.uploadMethod === 'new'
? `ipfs://${coverImageUrl}/${collectionDetails?.imageFile[0]?.name as string}`
: `${coverImageUrl}`,
}
const element = document.createElement('a')
const file = new Blob([JSON.stringify(details)], { type: 'text/plain' })
element.href = URL.createObjectURL(file)
element.download = `${
minterType === 'vending'
? collectionDetails?.name
? `${collectionDetails.name}-`
: ''
: openEditionMinterDetails?.collectionDetails
? `${openEditionMinterDetails.collectionDetails.name}-`
: ''
}configuration-${new Date().toLocaleString().replaceAll(',', '_')}.json`
document.body.appendChild(element) // Required for this to work in FireFox
element.click()
}
const importDetails = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files === null || event.target.files.length === 0) return toast.error('No files selected.')
const file = event.target.files[0]
const reader = new FileReader()
reader.onload = (e) => {
const contents = e.target?.result
const details = JSON.parse(contents as string)
setMinterType(details.minterType)
if (details.vendingMinterContractAddress) {
details.uploadDetails.uploadMethod = 'existing'
details.uploadDetails.baseTokenURI = details.baseTokenUri
details.uploadDetails.imageUrl = details.coverImageUrl
}
if (details.openEditionMinterDetails?.openEditionMinterContractAddress) {
details.openEditionMinterDetails.offChainMetadataUploadDetails.uploadMethod = 'existing'
details.openEditionMinterDetails.offChainMetadataUploadDetails.tokenURI =
details.openEditionMinterDetails.tokenUri
details.openEditionMinterDetails.offChainMetadataUploadDetails.imageUrl =
details.openEditionMinterDetails.coverImageUrl
}
setImportedDetails(details)
}
reader.readAsText(file)
}
const syncCollections = useCallback(async () => {
const collectionAddress =
minterType === 'openEdition' ? openEditionMinterCreatorData?.sg721ContractAddress : sg721ContractAddress
@ -1370,6 +1441,7 @@ const CollectionCreationPage: NextPage = () => {
on how to create your collection
</p>
</div>
<div className="mx-10" ref={scrollRef}>
<Conditional
test={minterType === 'openEdition' && openEditionMinterCreatorData?.openEditionMinterContractAddress !== null}
@ -1609,6 +1681,7 @@ const CollectionCreationPage: NextPage = () => {
'mx-10 mt-5',
'grid before:absolute relative grid-cols-3 grid-flow-col items-stretch rounded',
'before:inset-x-0 before:bottom-0 before:border-white/25',
minterType !== 'base' ? 'rounded-none border-b-2 border-white/25' : 'border-0',
)}
>
<div
@ -1682,6 +1755,22 @@ const CollectionCreationPage: NextPage = () => {
</div>
</div>
<Conditional test={minterType !== 'base'}>
<FormControl className={clsx('py-4 px-10 w-full')} title="Import Creation Configuration">
<div className="flex flex-row justify-between mt-5 space-x-2">
<input
accept="application/json"
className="py-4 px-4 w-1/3 rounded-sm border-[1px] border-zinc-500 border-dashed"
onChange={(e) => importDetails(e)}
type="file"
/>
<Button className="mt-3 h-1/2 w-1/8" onClick={() => exportDetails()}>
Export Creation Configuration
</Button>
</div>
</FormControl>
</Conditional>
{minterType === 'base' && (
<div>
<BaseMinterDetails minterType={minterType} onChange={setBaseMinterDetails} />
@ -1689,6 +1778,7 @@ const CollectionCreationPage: NextPage = () => {
)}
<Conditional test={minterType === 'openEdition'}>
<OpenEditionMinterCreator
importedOpenEditionMinterDetails={importedDetails?.openEditionMinterDetails}
minimumMintPrice={minimumOpenEditionMintPrice as string}
minimumUpdatableMintPrice={minimumOpenEditionUpdatableMintPrice as string}
mintTokenFromFactory={mintTokenFromOpenEditionFactory}
@ -1703,6 +1793,7 @@ const CollectionCreationPage: NextPage = () => {
<Conditional test={minterType === 'vending' || minterType === 'base'}>
<UploadDetails
baseMinterAcquisitionMethod={baseMinterDetails?.baseMinterAcquisitionMethod}
importedUploadDetails={importedDetails?.uploadDetails}
minterType={minterType}
onChange={setUploadDetails}
/>
@ -1723,6 +1814,7 @@ const CollectionCreationPage: NextPage = () => {
>
<CollectionDetails
coverImageUrl={coverImageUrl as string}
importedCollectionDetails={importedDetails?.collectionDetails}
minterType={minterType}
onChange={setCollectionDetails}
uploadMethod={uploadDetails?.uploadMethod as UploadMethod}
@ -1730,6 +1822,7 @@ const CollectionCreationPage: NextPage = () => {
</Conditional>
<Conditional test={minterType === 'vending'}>
<MintingDetails
importedMintingDetails={importedDetails?.mintingDetails}
minimumMintPrice={
whitelistDetails?.whitelistState !== 'none' && whitelistDetails?.whitelistType === 'flex'
? Number(minimumFlexMintPrice) / 1000000
@ -1766,10 +1859,14 @@ const CollectionCreationPage: NextPage = () => {
>
<div className="my-6">
<Conditional test={minterType === 'vending'}>
<WhitelistDetails mintingTokenFromFactory={mintTokenFromVendingFactory} onChange={setWhitelistDetails} />
<WhitelistDetails
importedWhitelistDetails={importedDetails?.whitelistDetails}
mintingTokenFromFactory={mintTokenFromVendingFactory}
onChange={setWhitelistDetails}
/>
<div className="my-6" />
</Conditional>
<RoyaltyDetails onChange={setRoyaltyDetails} />
<RoyaltyDetails importedRoyaltyDetails={importedDetails?.royaltyDetails} onChange={setRoyaltyDetails} />
</div>
</Conditional>
<Conditional test={readyToCreateVm && minterType === 'vending'}>