Merge development to main (#43)

* UI changes & improvements (#41)

* Remove show advanced options button, add symbol input

* Add checks for minting time constraints

* Royalty share input placeholder update

* Update input subtitles & error messages

* Token price subtitles now include minimum token price

* Ensure the files to be uploaded are in numerical order starting from 1

* Add collection creation confirmation modal

* Make some changes

Co-authored-by: findolor <anakisci@gmail.com>

* Airdrop feature added to collection actions (#42)

Co-authored-by: name-user1 <eray@deuslabs.fi>

Co-authored-by: Serkan Reis <serkanreis@gmail.com>
Co-authored-by: name-user1 <101495985+name-user1@users.noreply.github.com>
Co-authored-by: name-user1 <eray@deuslabs.fi>
This commit is contained in:
Arda Nakışçı 2022-09-01 09:27:23 +03:00 committed by GitHub
parent 00960f429e
commit 9dc19a99ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 234 additions and 77 deletions

View File

@ -0,0 +1,41 @@
import { Button } from './Button'
export interface ConfirmationModalProps {
confirm: () => void
}
export const ConfirmationModal = (props: ConfirmationModalProps) => {
return (
<div>
<input className="modal-toggle" id="my-modal-2" type="checkbox" />
<label className="cursor-pointer modal" htmlFor="my-modal-2">
<label
className="absolute top-[40%] bottom-5 left-1/3 max-w-xl max-h-40 border-2 no-scrollbar modal-box"
htmlFor="temp"
>
{/* <Alert type="warning"></Alert> */}
<div className="text-xl font-bold">
Are you sure to create a collection with the specified assets, metadata and parameters?
</div>
<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">
<label
className="w-full h-full text-white bg-gray-600 hover:bg-gray-600 border-0 btn modal-button"
htmlFor="my-modal-2"
>
Go Back
</label>
</Button>
<Button className="px-0 mt-4 mb-4 max-h-12" onClick={props.confirm}>
<label
className="w-full h-full text-white bg-plumbus-light hover:bg-plumbus-light border-0 btn modal-button"
htmlFor="my-modal-2"
>
Confirm
</label>
</Button>
</div>
</label>
</label>
</div>
)
}

View File

@ -18,6 +18,7 @@ export const ACTION_TYPES = [
'burn', 'burn',
'batch_burn', 'batch_burn',
'shuffle', 'shuffle',
'airdrop',
] as const ] as const
export interface ActionListItem { export interface ActionListItem {
@ -87,6 +88,11 @@ export const ACTION_LIST: ActionListItem[] = [
name: 'Shuffle Tokens', name: 'Shuffle Tokens',
description: 'Shuffle the token IDs', description: 'Shuffle the token IDs',
}, },
{
id: 'airdrop',
name: 'Airdrop Tokens',
description: 'Airdrop tokens to given addresses',
},
] ]
export interface DispatchExecuteProps { export interface DispatchExecuteProps {
@ -117,6 +123,7 @@ export type DispatchExecuteArgs = {
| { type: Select<'batch_transfer'>; recipient: string; tokenIds: string } | { type: Select<'batch_transfer'>; recipient: string; tokenIds: string }
| { type: Select<'burn'>; tokenId: number } | { type: Select<'burn'>; tokenId: number }
| { type: Select<'batch_burn'>; tokenIds: string } | { type: Select<'batch_burn'>; tokenIds: string }
| { type: Select<'airdrop'>; recipients: string[] }
) )
export const dispatchExecute = async (args: DispatchExecuteArgs) => { export const dispatchExecute = async (args: DispatchExecuteArgs) => {
@ -161,6 +168,9 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
case 'batch_burn': { case 'batch_burn': {
return sg721Messages.batchBurn(args.tokenIds) return sg721Messages.batchBurn(args.tokenIds)
} }
case 'airdrop': {
return minterMessages.airdrop(txSigner, args.recipients)
}
default: { default: {
throw new Error('Unknown action') throw new Error('Unknown action')
} }
@ -210,6 +220,9 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
case 'batch_burn': { case 'batch_burn': {
return sg721Messages(sg721Contract)?.batchBurn(args.tokenIds) return sg721Messages(sg721Contract)?.batchBurn(args.tokenIds)
} }
case 'airdrop': {
return minterMessages(minterContract)?.airdrop(args.recipients)
}
default: { default: {
return {} return {}
} }

View File

@ -23,6 +23,7 @@ interface CollectionDetailsProps {
export interface CollectionDetailsDataProps { export interface CollectionDetailsDataProps {
name: string name: string
description: string description: string
symbol: string
imageFile: File[] imageFile: File[]
externalLink?: string externalLink?: string
} }
@ -44,10 +45,17 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
placeholder: 'My Awesome Collection Description', placeholder: 'My Awesome Collection Description',
}) })
const symbolState = useInputState({
id: 'symbol',
name: 'symbol',
title: 'Symbol',
placeholder: 'SYMBOL',
})
const externalLinkState = useInputState({ const externalLinkState = useInputState({
id: 'external-link', id: 'external-link',
name: 'externalLink', name: 'externalLink',
title: 'External Link', title: 'External Link (optional)',
placeholder: 'https://my-collection...', placeholder: 'https://my-collection...',
}) })
@ -56,6 +64,7 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
const data: CollectionDetailsDataProps = { const data: CollectionDetailsDataProps = {
name: nameState.value, name: nameState.value,
description: descriptionState.value, description: descriptionState.value,
symbol: symbolState.value,
imageFile: coverImage ? [coverImage] : [], imageFile: coverImage ? [coverImage] : [],
externalLink: externalLinkState.value, externalLink: externalLinkState.value,
} }
@ -88,6 +97,7 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
<FormGroup subtitle="Information about your collection" title="Collection Details"> <FormGroup subtitle="Information about your collection" title="Collection Details">
<TextInput {...nameState} isRequired /> <TextInput {...nameState} isRequired />
<TextInput {...descriptionState} isRequired /> <TextInput {...descriptionState} isRequired />
<TextInput {...symbolState} isRequired />
<FormControl isRequired={uploadMethod === 'new'} title="Cover Image"> <FormControl isRequired={uploadMethod === 'new'} title="Cover Image">
{uploadMethod === 'new' && ( {uploadMethod === 'new' && (

View File

@ -35,8 +35,8 @@ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: Minti
id: 'unitPrice', id: 'unitPrice',
name: 'unitPrice', name: 'unitPrice',
title: 'Unit Price', title: 'Unit Price',
subtitle: '', subtitle: 'Price of each token (min. 50 STARS)',
placeholder: '100', placeholder: '50',
}) })
const perAddressLimitState = useNumberInputState({ const perAddressLimitState = useNumberInputState({

View File

@ -1,6 +1,6 @@
import { Conditional } from 'components/Conditional' import { Conditional } from 'components/Conditional'
import { FormGroup } from 'components/FormGroup' import { FormGroup } from 'components/FormGroup'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import { useInputState } from 'components/forms/FormInput.hooks'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { NumberInput, TextInput } from '../../forms/FormInput' import { NumberInput, TextInput } from '../../forms/FormInput'
@ -28,19 +28,19 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...', placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
}) })
const royaltyShareState = useNumberInputState({ const royaltyShareState = useInputState({
id: 'royalty-share', id: 'royalty-share',
name: 'royaltyShare', name: 'royaltyShare',
title: 'Share Percentage', title: 'Share Percentage',
subtitle: 'Percentage of royalties to be paid', subtitle: 'Percentage of royalties to be paid',
placeholder: '8', placeholder: '8%',
}) })
useEffect(() => { useEffect(() => {
const data: RoyaltyDetailsDataProps = { const data: RoyaltyDetailsDataProps = {
royaltyType: royaltyState, royaltyType: royaltyState,
paymentAddress: royaltyPaymentAddressState.value, paymentAddress: royaltyPaymentAddressState.value,
share: royaltyShareState.value, share: Number(royaltyShareState.value),
} }
onChange(data) onChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -84,8 +84,8 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
</div> </div>
<Conditional test={royaltyState === 'new'}> <Conditional test={royaltyState === 'new'}>
<FormGroup subtitle="Information about royalty" title="Royalty Details"> <FormGroup subtitle="Information about royalty" title="Royalty Details">
<TextInput {...royaltyPaymentAddressState} /> <TextInput {...royaltyPaymentAddressState} isRequired />
<NumberInput {...royaltyShareState} /> <NumberInput {...royaltyShareState} isRequired />
</FormGroup> </FormGroup>
</Conditional> </Conditional>
</div> </div>

View File

@ -78,11 +78,23 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
}) })
const selectAssets = (event: ChangeEvent<HTMLInputElement>) => { const selectAssets = (event: ChangeEvent<HTMLInputElement>) => {
const files: File[] = []
let reader: FileReader
if (event.target.files === null) return
setAssetFilesArray([]) setAssetFilesArray([])
setMetadataFilesArray([]) setMetadataFilesArray([])
if (event.target.files === null) return
//sort the files
const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name))
//check if the sorted file names are in numerical order
const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0])
for (let i = 0; i < sortedFileNames.length; i++) {
if (parseInt(sortedFileNames[i]) !== i + 1) {
toast.error('The file names should be in numerical order starting from 1.')
//clear the input
event.target.value = ''
return
}
}
const files: File[] = []
let reader: FileReader
for (let i = 0; i < event.target.files.length; i++) { for (let i = 0; i < event.target.files.length; i++) {
reader = new FileReader() reader = new FileReader()
reader.onload = (e) => { reader.onload = (e) => {
@ -102,10 +114,26 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
} }
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => { const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
setMetadataFilesArray([])
if (event.target.files === null) return toast.error('No files selected.')
if (event.target.files.length !== assetFilesArray.length) {
event.target.value = ''
return toast.error('The number of metadata files should be equal to the number of asset files.')
}
//sort the files
const sortedFiles = Array.from(event.target.files).sort((a, b) => naturalCompare(a.name, b.name))
//check if the sorted file names are in numerical order
const sortedFileNames = sortedFiles.map((file) => file.name.split('.')[0])
for (let i = 0; i < sortedFileNames.length; i++) {
if (parseInt(sortedFileNames[i]) !== i + 1) {
toast.error('The file names should be in numerical order starting from 1.')
//clear the input
event.target.value = ''
return
}
}
const files: File[] = [] const files: File[] = []
let reader: FileReader let reader: FileReader
if (event.target.files === null) return toast.error('No files selected.')
setMetadataFilesArray([])
for (let i = 0; i < event.target.files.length; i++) { for (let i = 0; i < event.target.files.length; i++) {
reader = new FileReader() reader = new FileReader()
reader.onload = (e) => { reader.onload = (e) => {

View File

@ -43,15 +43,15 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
id: 'unit-price', id: 'unit-price',
name: 'unitPrice', name: 'unitPrice',
title: 'Unit Price', title: 'Unit Price',
subtitle: 'Price of each tokens in collection', subtitle: 'Token price for whitelisted addresses \n (min. 25 STARS)',
placeholder: '500', placeholder: '25',
}) })
const memberLimitState = useNumberInputState({ const memberLimitState = useNumberInputState({
id: 'member-limit', id: 'member-limit',
name: 'memberLimit', name: 'memberLimit',
title: 'Member Limit', title: 'Member Limit',
subtitle: 'Limit of the whitelisted members', subtitle: 'Maximum number of whitelisted addresses',
placeholder: '1000', placeholder: '1000',
}) })
@ -59,7 +59,7 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
id: 'per-address-limit', id: 'per-address-limit',
name: 'perAddressLimit', name: 'perAddressLimit',
title: 'Per Address Limit', title: 'Per Address Limit',
subtitle: 'Limit of tokens per address', subtitle: 'Maximum number of tokens per whitelisted address',
placeholder: '5', placeholder: '5',
}) })
@ -145,11 +145,7 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
</div> </div>
<Conditional test={whitelistState === 'existing'}> <Conditional test={whitelistState === 'existing'}>
<AddressInput <AddressInput {...whitelistAddressState} className="pb-5" isRequired />
{...whitelistAddressState}
className="pb-5"
onChange={(e) => whitelistAddressState.onChange(e.target.value)}
/>
</Conditional> </Conditional>
<Conditional test={whitelistState === 'new'}> <Conditional test={whitelistState === 'new'}>
@ -158,10 +154,20 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
<NumberInput isRequired {...uniPriceState} /> <NumberInput isRequired {...uniPriceState} />
<NumberInput isRequired {...memberLimitState} /> <NumberInput isRequired {...memberLimitState} />
<NumberInput isRequired {...perAddressLimitState} /> <NumberInput isRequired {...perAddressLimitState} />
<FormControl htmlId="start-date" isRequired subtitle="Start time for the minting" title="Start Time"> <FormControl
htmlId="start-date"
isRequired
subtitle="Start time for minting tokens to whitelisted addresses"
title="Start Time"
>
<InputDateTime minDate={new Date()} onChange={(date) => setStartDate(date)} value={startDate} /> <InputDateTime minDate={new Date()} onChange={(date) => setStartDate(date)} value={startDate} />
</FormControl> </FormControl>
<FormControl htmlId="end-date" isRequired subtitle="End time for the minting" title="End Time"> <FormControl
htmlId="end-date"
isRequired
subtitle="End time for minting tokens to whitelisted addresses"
title="End Time"
>
<InputDateTime minDate={new Date()} onChange={(date) => setEndDate(date)} value={endDate} /> <InputDateTime minDate={new Date()} onChange={(date) => setEndDate(date)} value={endDate} />
</FormControl> </FormControl>
</FormGroup> </FormGroup>

View File

@ -37,6 +37,7 @@ export interface MinterInstance {
batchMint: (senderAddress: string, recipient: string, batchNumber: number) => Promise<string> batchMint: (senderAddress: string, recipient: string, batchNumber: number) => Promise<string>
shuffle: (senderAddress: string) => Promise<string> shuffle: (senderAddress: string) => Promise<string>
withdraw: (senderAddress: string) => Promise<string> withdraw: (senderAddress: string) => Promise<string>
airdrop: (senderAddress: string, recipients: string[]) => Promise<string>
} }
export interface MinterMessages { export interface MinterMessages {
@ -46,9 +47,10 @@ export interface MinterMessages {
updatePerAddressLimit: (perAddressLimit: number) => UpdatePerAddressLimitMessage updatePerAddressLimit: (perAddressLimit: number) => UpdatePerAddressLimitMessage
mintTo: (recipient: string) => MintToMessage mintTo: (recipient: string) => MintToMessage
mintFor: (recipient: string, tokenId: number) => MintForMessage mintFor: (recipient: string, tokenId: number) => MintForMessage
batchMint: (recipient: string, batchNumber: number) => BatchMintMessage batchMint: (recipient: string, batchNumber: number) => CustomMessage
shuffle: () => ShuffleMessage shuffle: () => ShuffleMessage
withdraw: () => WithdrawMessage withdraw: () => WithdrawMessage
airdrop: (recipients: string[]) => CustomMessage
} }
export interface MintMessage { export interface MintMessage {
@ -114,7 +116,7 @@ export interface MintForMessage {
funds: Coin[] funds: Coin[]
} }
export interface BatchMintMessage { export interface CustomMessage {
sender: string sender: string
contract: string contract: string
msg: Record<string, unknown>[] msg: Record<string, unknown>[]
@ -301,6 +303,29 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC
return res.transactionHash return res.transactionHash
} }
const airdrop = async (senderAddress: string, recipients: string[]): Promise<string> => {
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
for (let i = 0; i < recipients.length; i++) {
const msg = {
mint_to: { recipient: recipients[i] },
}
const executeContractMsg: MsgExecuteContractEncodeObject = {
typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
value: MsgExecuteContract.fromPartial({
sender: senderAddress,
contract: contractAddress,
msg: toUtf8(JSON.stringify(msg)),
}),
}
executeContractMsgs.push(executeContractMsg)
}
const res = await client.signAndBroadcast(senderAddress, executeContractMsgs, 'auto', 'airdrop')
return res.transactionHash
}
const shuffle = async (senderAddress: string): Promise<string> => { const shuffle = async (senderAddress: string): Promise<string> => {
const res = await client.execute( const res = await client.execute(
senderAddress, senderAddress,
@ -343,6 +368,7 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC
mintTo, mintTo,
mintFor, mintFor,
batchMint, batchMint,
airdrop,
shuffle, shuffle,
withdraw, withdraw,
} }
@ -441,7 +467,7 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC
} }
} }
const batchMint = (recipient: string, batchNumber: number): BatchMintMessage => { const batchMint = (recipient: string, batchNumber: number): CustomMessage => {
const msg: Record<string, unknown>[] = [] const msg: Record<string, unknown>[] = []
for (let i = 0; i < batchNumber; i++) { for (let i = 0; i < batchNumber; i++) {
msg.push({ mint_to: { recipient } }) msg.push({ mint_to: { recipient } })
@ -454,6 +480,19 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC
} }
} }
const airdrop = (recipients: string[]): CustomMessage => {
const msg: Record<string, unknown>[] = []
for (let i = 0; i < recipients.length; i++) {
msg.push({ mint_to: { recipient: recipients[i] } })
}
return {
sender: txSigner,
contract: contractAddress,
msg,
funds: [],
}
}
const shuffle = (): ShuffleMessage => { const shuffle = (): ShuffleMessage => {
return { return {
sender: txSigner, sender: txSigner,
@ -484,6 +523,7 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC
mintTo, mintTo,
mintFor, mintFor,
batchMint, batchMint,
airdrop,
shuffle, shuffle,
withdraw, withdraw,
} }

View File

@ -22,6 +22,7 @@
"@fontsource/roboto": "^4", "@fontsource/roboto": "^4",
"@headlessui/react": "^1", "@headlessui/react": "^1",
"@keplr-wallet/cosmos": "^0.9.16", "@keplr-wallet/cosmos": "^0.9.16",
"@pinata/sdk": "^1.1.26",
"@popperjs/core": "^2", "@popperjs/core": "^2",
"@svgr/webpack": "^6", "@svgr/webpack": "^6",
"@tailwindcss/forms": "^0", "@tailwindcss/forms": "^0",
@ -34,7 +35,6 @@
"next": "^12", "next": "^12",
"next-seo": "^4", "next-seo": "^4",
"nft.storage": "^6.3.0", "nft.storage": "^6.3.0",
"@pinata/sdk": "^1.1.26",
"react": "^18", "react": "^18",
"react-datetime-picker": "^3", "react-datetime-picker": "^3",
"react-dom": "^18", "react-dom": "^18",
@ -44,7 +44,6 @@
"react-popper": "^2", "react-popper": "^2",
"react-query": "^3", "react-query": "^3",
"react-tracked": "^1", "react-tracked": "^1",
"react-collapsed": "^3",
"scheduler": "^0", "scheduler": "^0",
"zustand": "^3" "zustand": "^3"
}, },

View File

@ -6,11 +6,13 @@ import { useActionsComboboxState } from 'components/collections/actions/Combobox
import { Conditional } from 'components/Conditional' import { Conditional } from 'components/Conditional'
import { ContractPageHeader } from 'components/ContractPageHeader' import { ContractPageHeader } from 'components/ContractPageHeader'
import { FormControl } from 'components/FormControl' import { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup'
import { AddressInput, NumberInput } from 'components/forms/FormInput' import { AddressInput, NumberInput } from 'components/forms/FormInput'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks' import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime' import { InputDateTime } from 'components/InputDateTime'
import { JsonPreview } from 'components/JsonPreview' import { JsonPreview } from 'components/JsonPreview'
import { TransactionHash } from 'components/TransactionHash' import { TransactionHash } from 'components/TransactionHash'
import { WhitelistUpload } from 'components/WhitelistUpload'
import { useContracts } from 'contexts/contracts' import { useContracts } from 'contexts/contracts'
import { useWallet } from 'contexts/wallet' import { useWallet } from 'contexts/wallet'
import type { NextPage } from 'next' import type { NextPage } from 'next'
@ -31,6 +33,7 @@ const CollectionActionsPage: NextPage = () => {
const [lastTx, setLastTx] = useState('') const [lastTx, setLastTx] = useState('')
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined) const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
const [airdropArray, setAirdropArray] = useState<string[]>([])
const comboboxState = useActionsComboboxState() const comboboxState = useActionsComboboxState()
const type = comboboxState.value?.id const type = comboboxState.value?.id
@ -99,6 +102,7 @@ const CollectionActionsPage: NextPage = () => {
const showNumberOfTokensField = type === 'batch_mint' const showNumberOfTokensField = type === 'batch_mint'
const showTokenIdListField = isEitherType(type, ['batch_burn', 'batch_transfer']) const showTokenIdListField = isEitherType(type, ['batch_burn', 'batch_transfer'])
const showRecipientField = isEitherType(type, ['transfer', 'mint_to', 'mint_for', 'batch_mint', 'batch_transfer']) const showRecipientField = isEitherType(type, ['transfer', 'mint_to', 'mint_for', 'batch_mint', 'batch_transfer'])
const showAirdropFileField = type === 'airdrop'
const minterMessages = useMemo( const minterMessages = useMemo(
() => minterContract?.use(minterContractState.value), () => minterContract?.use(minterContractState.value),
@ -120,6 +124,7 @@ const CollectionActionsPage: NextPage = () => {
minterMessages, minterMessages,
sg721Messages, sg721Messages,
recipient: recipientState.value, recipient: recipientState.value,
recipients: airdropArray,
txSigner: wallet.address, txSigner: wallet.address,
type, type,
} }
@ -148,6 +153,10 @@ const CollectionActionsPage: NextPage = () => {
}, },
) )
const airdropFileOnChange = (data: string[]) => {
setAirdropArray(data)
}
return ( return (
<section className="py-6 px-12 space-y-4"> <section className="py-6 px-12 space-y-4">
<NextSeo title="Collection Actions" /> <NextSeo title="Collection Actions" />
@ -168,6 +177,11 @@ const CollectionActionsPage: NextPage = () => {
{showTokenIdField && <NumberInput {...tokenIdState} />} {showTokenIdField && <NumberInput {...tokenIdState} />}
{showTokenIdListField && <TextInput {...tokenIdListState} />} {showTokenIdListField && <TextInput {...tokenIdListState} />}
{showNumberOfTokensField && <NumberInput {...batchNumberState} />} {showNumberOfTokensField && <NumberInput {...batchNumberState} />}
{showAirdropFileField && (
<FormGroup subtitle="TXT file that contains the airdrop addresses" title="Airdrop File">
<WhitelistUpload onChange={airdropFileOnChange} />
</FormGroup>
)}
<Conditional test={showDateField}> <Conditional test={showDateField}>
<FormControl htmlId="start-date" subtitle="Start time for the minting" title="Start Time"> <FormControl htmlId="start-date" subtitle="Start time for the minting" title="Start Time">
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} /> <InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />

View File

@ -26,7 +26,6 @@ import { useWallet } from 'contexts/wallet'
import type { NextPage } from 'next' import type { NextPage } from 'next'
import { NextSeo } from 'next-seo' import { NextSeo } from 'next-seo'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import useCollapse from 'react-collapsed'
import { toast } from 'react-hot-toast' 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'
@ -35,15 +34,12 @@ import { withMetadata } from 'utils/layout'
import { links } from 'utils/links' import { links } from 'utils/links'
import type { UploadMethod } from '../../components/collections/creation/UploadDetails' import type { UploadMethod } from '../../components/collections/creation/UploadDetails'
import { ConfirmationModal } from '../../components/ConfirmationModal'
import { getAssetType } from '../../utils/getAssetType' import { getAssetType } from '../../utils/getAssetType'
const CollectionCreationPage: NextPage = () => { const CollectionCreationPage: NextPage = () => {
const wallet = useWallet() const wallet = useWallet()
const { minter: minterContract, whitelist: whitelistContract } = useContracts() const { minter: minterContract, whitelist: whitelistContract } = useContracts()
const { getCollapseProps, getToggleProps, isExpanded } = useCollapse()
const toggleProps = getToggleProps()
const collapseProps = getCollapseProps()
const scrollRef = useRef<HTMLDivElement>(null) const scrollRef = useRef<HTMLDivElement>(null)
const [uploadDetails, setUploadDetails] = useState<UploadDetailsDataProps | null>(null) const [uploadDetails, setUploadDetails] = useState<UploadDetailsDataProps | null>(null)
@ -53,12 +49,29 @@ const CollectionCreationPage: NextPage = () => {
const [royaltyDetails, setRoyaltyDetails] = useState<RoyaltyDetailsDataProps | null>(null) const [royaltyDetails, setRoyaltyDetails] = useState<RoyaltyDetailsDataProps | null>(null)
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState(false)
const [readyToCreate, setReadyToCreate] = useState(false)
const [minterContractAddress, setMinterContractAddress] = useState<string | null>(null) const [minterContractAddress, setMinterContractAddress] = useState<string | null>(null)
const [sg721ContractAddress, setSg721ContractAddress] = useState<string | null>(null) const [sg721ContractAddress, setSg721ContractAddress] = useState<string | null>(null)
const [baseTokenUri, setBaseTokenUri] = useState<string | null>(null) const [baseTokenUri, setBaseTokenUri] = useState<string | null>(null)
const [coverImageUrl, setCoverImageUrl] = useState<string | null>(null) const [coverImageUrl, setCoverImageUrl] = useState<string | null>(null)
const [transactionHash, setTransactionHash] = useState<string | null>(null) const [transactionHash, setTransactionHash] = useState<string | null>(null)
const performChecks = () => {
try {
// setReadyToCreate(false)
// checkUploadDetails()
// checkCollectionDetails()
// checkMintingDetails()
// checkWhitelistDetails()
// checkRoyaltyDetails()
setReadyToCreate(true)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
toast.error(error.message)
setUploading(false)
}
}
const createCollection = async () => { const createCollection = async () => {
try { try {
setBaseTokenUri(null) setBaseTokenUri(null)
@ -66,11 +79,6 @@ const CollectionCreationPage: NextPage = () => {
setMinterContractAddress(null) setMinterContractAddress(null)
setSg721ContractAddress(null) setSg721ContractAddress(null)
setTransactionHash(null) setTransactionHash(null)
checkUploadDetails()
checkCollectionDetails()
checkMintingDetails()
checkWhitelistDetails()
checkRoyaltyDetails()
if (uploadDetails?.uploadMethod === 'new') { if (uploadDetails?.uploadMethod === 'new') {
setUploading(true) setUploading(true)
@ -84,22 +92,27 @@ const CollectionCreationPage: NextPage = () => {
uploadDetails.pinataApiKey as string, uploadDetails.pinataApiKey as string,
uploadDetails.pinataSecretKey as string, uploadDetails.pinataSecretKey as string,
) )
setUploading(false) setUploading(false)
setBaseTokenUri(baseUri) setBaseTokenUri(baseUri)
setCoverImageUrl(coverImageUri) setCoverImageUrl(coverImageUri)
let whitelist: string | undefined let whitelist: string | undefined
if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress
else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist()
await instantiate(baseUri, coverImageUri, whitelist) 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 let whitelist: string | undefined
if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress
else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist()
await instantiate(baseTokenUri as string, coverImageUrl as string, whitelist) await instantiate(baseTokenUri as string, coverImageUrl as string, whitelist)
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) { } catch (error: any) {
toast.error(error.message) toast.error(error.message)
@ -141,13 +154,14 @@ const CollectionCreationPage: NextPage = () => {
share: (Number(royaltyDetails.share) / 100).toString(), share: (Number(royaltyDetails.share) / 100).toString(),
} }
} }
const msg = { const msg = {
base_token_uri: `${uploadDetails?.uploadMethod === 'new' ? `ipfs://${baseUri}/` : `${baseUri}`}`, base_token_uri: `${uploadDetails?.uploadMethod === 'new' ? `ipfs://${baseUri}/` : `${baseUri}`}`,
num_tokens: mintingDetails?.numTokens, num_tokens: mintingDetails?.numTokens,
sg721_code_id: SG721_CODE_ID, sg721_code_id: SG721_CODE_ID,
sg721_instantiate_msg: { sg721_instantiate_msg: {
name: collectionDetails?.name, name: collectionDetails?.name,
symbol: 'SYMBOL', symbol: collectionDetails?.symbol,
minter: wallet.address, minter: wallet.address,
collection_info: { collection_info: {
creator: wallet.address, creator: wallet.address,
@ -187,20 +201,25 @@ const CollectionCreationPage: NextPage = () => {
.then((assetUri: string) => { .then((assetUri: string) => {
const fileArray: File[] = [] const fileArray: File[] = []
let reader: FileReader let reader: FileReader
for (let i = 0; i < uploadDetails.metadataFiles.length; i++) { for (let i = 0; i < uploadDetails.metadataFiles.length; i++) {
reader = new FileReader() reader = new FileReader()
reader.onload = (e) => { reader.onload = (e) => {
const data: any = JSON.parse(e.target?.result as string) const data: any = JSON.parse(e.target?.result as string)
if ( if (
getAssetType(uploadDetails.assetFiles[i].name) === 'audio' || getAssetType(uploadDetails.assetFiles[i].name) === 'audio' ||
getAssetType(uploadDetails.assetFiles[i].name) === 'video' getAssetType(uploadDetails.assetFiles[i].name) === 'video'
) { ) {
data.animation_url = `ipfs://${assetUri}/${uploadDetails.assetFiles[i].name}` data.animation_url = `ipfs://${assetUri}/${uploadDetails.assetFiles[i].name}`
} }
data.image = `ipfs://${assetUri}/${uploadDetails.assetFiles[i].name}` data.image = `ipfs://${assetUri}/${uploadDetails.assetFiles[i].name}`
const metadataFileBlob = new Blob([JSON.stringify(data)], { const metadataFileBlob = new Blob([JSON.stringify(data)], {
type: 'application/json', type: 'application/json',
}) })
const updatedMetadataFile = new File( const updatedMetadataFile = new File(
[metadataFileBlob], [metadataFileBlob],
uploadDetails.metadataFiles[i].name.substring(0, uploadDetails.metadataFiles[i].name.lastIndexOf('.')), uploadDetails.metadataFiles[i].name.substring(0, uploadDetails.metadataFiles[i].name.lastIndexOf('.')),
@ -208,6 +227,7 @@ const CollectionCreationPage: NextPage = () => {
type: 'application/json', type: 'application/json',
}, },
) )
fileArray.push(updatedMetadataFile) fileArray.push(updatedMetadataFile)
} }
reader.onloadend = () => { reader.onloadend = () => {
@ -286,6 +306,7 @@ const CollectionCreationPage: NextPage = () => {
) )
throw new Error('Invalid limit for tokens per address') throw new Error('Invalid limit for tokens per address')
if (mintingDetails.startTime === '') throw new Error('Start time is required') if (mintingDetails.startTime === '') throw new Error('Start time is required')
if (Number(mintingDetails.startTime) < new Date().getTime() * 1000000) throw new Error('Invalid start time')
} }
const checkWhitelistDetails = () => { const checkWhitelistDetails = () => {
@ -301,14 +322,18 @@ const CollectionCreationPage: NextPage = () => {
if (whitelistDetails.endTime === '') throw new Error('End time is required') if (whitelistDetails.endTime === '') throw new Error('End time is required')
if (whitelistDetails.perAddressLimit === 0) throw new Error('Per address limit is required') if (whitelistDetails.perAddressLimit === 0) throw new Error('Per address limit is required')
if (whitelistDetails.memberLimit === 0) throw new Error('Member limit is required') if (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 later than whitelist end time')
if (Number(whitelistDetails.endTime) > Number(mintingDetails?.startTime))
throw new Error('Whitelist end time cannot be later than public start time')
} }
} }
const checkRoyaltyDetails = () => { const checkRoyaltyDetails = () => {
if (!royaltyDetails) throw new Error('Please fill out the royalty details') if (!royaltyDetails) throw new Error('Please fill out the royalty details')
if (royaltyDetails.royaltyType === 'new') { if (royaltyDetails.royaltyType === 'new') {
if (royaltyDetails.share === 0) throw new Error('Royalty share is required') 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') 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 (royaltyDetails.paymentAddress === '') throw new Error('Royalty payment address is required')
} }
} }
@ -411,21 +436,22 @@ const CollectionCreationPage: NextPage = () => {
uploadMethod={uploadDetails?.uploadMethod as UploadMethod} uploadMethod={uploadDetails?.uploadMethod as UploadMethod}
/> />
</div> </div>
<div className="my-6">
<div className="flex justify-between my-6">
<Button {...toggleProps} isWide type="button" variant="outline">
{isExpanded ? 'Hide' : 'Show'} Advanced Details
</Button>
<Button isWide onClick={createCollection} variant="solid">
Create Collection
</Button>
</div>
<section {...collapseProps} className="mb-10">
<WhitelistDetails onChange={setWhitelistDetails} /> <WhitelistDetails onChange={setWhitelistDetails} />
<div className="my-6" /> <div className="my-6" />
<RoyaltyDetails onChange={setRoyaltyDetails} /> <RoyaltyDetails onChange={setRoyaltyDetails} />
</section> </div>
{readyToCreate && <ConfirmationModal confirm={createCollection} />}
<div className="flex justify-end w-full">
<Button className="px-0 mb-6 max-h-12" onClick={performChecks} variant="solid">
<label
className="relative justify-end w-full h-full text-white bg-plumbus-light hover:bg-plumbus-light border-0 btn modal-button"
htmlFor="my-modal-2"
>
Create Collection
</label>
</Button>
</div>
</div> </div>
</div> </div>
) )

View File

@ -6541,11 +6541,6 @@ pbkdf2@^3.0.16, pbkdf2@^3.0.9, pbkdf2@^3.1.1:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
sha.js "^2.4.8" sha.js "^2.4.8"
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
picocolors@^1.0.0: picocolors@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz"
@ -6725,13 +6720,6 @@ rabin-wasm@^0.1.4:
node-fetch "^2.6.1" node-fetch "^2.6.1"
readable-stream "^3.6.0" readable-stream "^3.6.0"
raf@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
dependencies:
performance-now "^2.1.0"
randombytes@^2.0.1: randombytes@^2.0.1:
version "2.1.0" version "2.1.0"
resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz"
@ -6759,14 +6747,6 @@ react-clock@^3.0.0:
merge-class-names "^1.1.1" merge-class-names "^1.1.1"
prop-types "^15.6.0" prop-types "^15.6.0"
react-collapsed@^3:
version "3.3.2"
resolved "https://registry.yarnpkg.com/react-collapsed/-/react-collapsed-3.3.2.tgz#b04dd13db5370ef553feac6556c911bb41dc63e2"
integrity sha512-P3YmP0k3Z7LaELQP2CbXjWSjiaNEIBvvxFUAJToJwztMFrTdHe9v7vQMz3FtMTyxLDJc9eGdNA8qMxCe9Aj3tg==
dependencies:
raf "^3.4.1"
tiny-warning "^1.0.3"
react-date-picker@^8.4.0: react-date-picker@^8.4.0:
version "8.4.0" version "8.4.0"
resolved "https://registry.npmjs.org/react-date-picker/-/react-date-picker-8.4.0.tgz" resolved "https://registry.npmjs.org/react-date-picker/-/react-date-picker-8.4.0.tgz"
@ -7585,7 +7565,7 @@ tiny-secp256k1@^1.1.3:
elliptic "^6.4.0" elliptic "^6.4.0"
nan "^2.13.2" nan "^2.13.2"
tiny-warning@^1.0.0, tiny-warning@^1.0.3: tiny-warning@^1.0.0:
version "1.0.3" version "1.0.3"
resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==