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:
parent
00960f429e
commit
9dc19a99ab
41
components/ConfirmationModal.tsx
Normal file
41
components/ConfirmationModal.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -18,6 +18,7 @@ export const ACTION_TYPES = [
|
||||
'burn',
|
||||
'batch_burn',
|
||||
'shuffle',
|
||||
'airdrop',
|
||||
] as const
|
||||
|
||||
export interface ActionListItem {
|
||||
@ -87,6 +88,11 @@ export const ACTION_LIST: ActionListItem[] = [
|
||||
name: 'Shuffle Tokens',
|
||||
description: 'Shuffle the token IDs',
|
||||
},
|
||||
{
|
||||
id: 'airdrop',
|
||||
name: 'Airdrop Tokens',
|
||||
description: 'Airdrop tokens to given addresses',
|
||||
},
|
||||
]
|
||||
|
||||
export interface DispatchExecuteProps {
|
||||
@ -117,6 +123,7 @@ export type DispatchExecuteArgs = {
|
||||
| { type: Select<'batch_transfer'>; recipient: string; tokenIds: string }
|
||||
| { type: Select<'burn'>; tokenId: number }
|
||||
| { type: Select<'batch_burn'>; tokenIds: string }
|
||||
| { type: Select<'airdrop'>; recipients: string[] }
|
||||
)
|
||||
|
||||
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
@ -161,6 +168,9 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
case 'batch_burn': {
|
||||
return sg721Messages.batchBurn(args.tokenIds)
|
||||
}
|
||||
case 'airdrop': {
|
||||
return minterMessages.airdrop(txSigner, args.recipients)
|
||||
}
|
||||
default: {
|
||||
throw new Error('Unknown action')
|
||||
}
|
||||
@ -210,6 +220,9 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
case 'batch_burn': {
|
||||
return sg721Messages(sg721Contract)?.batchBurn(args.tokenIds)
|
||||
}
|
||||
case 'airdrop': {
|
||||
return minterMessages(minterContract)?.airdrop(args.recipients)
|
||||
}
|
||||
default: {
|
||||
return {}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ interface CollectionDetailsProps {
|
||||
export interface CollectionDetailsDataProps {
|
||||
name: string
|
||||
description: string
|
||||
symbol: string
|
||||
imageFile: File[]
|
||||
externalLink?: string
|
||||
}
|
||||
@ -44,10 +45,17 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
|
||||
placeholder: 'My Awesome Collection Description',
|
||||
})
|
||||
|
||||
const symbolState = useInputState({
|
||||
id: 'symbol',
|
||||
name: 'symbol',
|
||||
title: 'Symbol',
|
||||
placeholder: 'SYMBOL',
|
||||
})
|
||||
|
||||
const externalLinkState = useInputState({
|
||||
id: 'external-link',
|
||||
name: 'externalLink',
|
||||
title: 'External Link',
|
||||
title: 'External Link (optional)',
|
||||
placeholder: 'https://my-collection...',
|
||||
})
|
||||
|
||||
@ -56,6 +64,7 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
|
||||
const data: CollectionDetailsDataProps = {
|
||||
name: nameState.value,
|
||||
description: descriptionState.value,
|
||||
symbol: symbolState.value,
|
||||
imageFile: coverImage ? [coverImage] : [],
|
||||
externalLink: externalLinkState.value,
|
||||
}
|
||||
@ -88,6 +97,7 @@ export const CollectionDetails = ({ onChange, uploadMethod, coverImageUrl }: Col
|
||||
<FormGroup subtitle="Information about your collection" title="Collection Details">
|
||||
<TextInput {...nameState} isRequired />
|
||||
<TextInput {...descriptionState} isRequired />
|
||||
<TextInput {...symbolState} isRequired />
|
||||
|
||||
<FormControl isRequired={uploadMethod === 'new'} title="Cover Image">
|
||||
{uploadMethod === 'new' && (
|
||||
|
@ -35,8 +35,8 @@ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod }: Minti
|
||||
id: 'unitPrice',
|
||||
name: 'unitPrice',
|
||||
title: 'Unit Price',
|
||||
subtitle: '',
|
||||
placeholder: '100',
|
||||
subtitle: 'Price of each token (min. 50 STARS)',
|
||||
placeholder: '50',
|
||||
})
|
||||
|
||||
const perAddressLimitState = useNumberInputState({
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Conditional } from 'components/Conditional'
|
||||
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 { NumberInput, TextInput } from '../../forms/FormInput'
|
||||
@ -28,19 +28,19 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
|
||||
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
|
||||
})
|
||||
|
||||
const royaltyShareState = useNumberInputState({
|
||||
const royaltyShareState = useInputState({
|
||||
id: 'royalty-share',
|
||||
name: 'royaltyShare',
|
||||
title: 'Share Percentage',
|
||||
subtitle: 'Percentage of royalties to be paid',
|
||||
placeholder: '8',
|
||||
placeholder: '8%',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const data: RoyaltyDetailsDataProps = {
|
||||
royaltyType: royaltyState,
|
||||
paymentAddress: royaltyPaymentAddressState.value,
|
||||
share: royaltyShareState.value,
|
||||
share: Number(royaltyShareState.value),
|
||||
}
|
||||
onChange(data)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -84,8 +84,8 @@ export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
|
||||
</div>
|
||||
<Conditional test={royaltyState === 'new'}>
|
||||
<FormGroup subtitle="Information about royalty" title="Royalty Details">
|
||||
<TextInput {...royaltyPaymentAddressState} />
|
||||
<NumberInput {...royaltyShareState} />
|
||||
<TextInput {...royaltyPaymentAddressState} isRequired />
|
||||
<NumberInput {...royaltyShareState} isRequired />
|
||||
</FormGroup>
|
||||
</Conditional>
|
||||
</div>
|
||||
|
@ -78,11 +78,23 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
|
||||
})
|
||||
|
||||
const selectAssets = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const files: File[] = []
|
||||
let reader: FileReader
|
||||
if (event.target.files === null) return
|
||||
setAssetFilesArray([])
|
||||
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++) {
|
||||
reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
@ -102,10 +114,26 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => {
|
||||
}
|
||||
|
||||
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[] = []
|
||||
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++) {
|
||||
reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
|
@ -43,15 +43,15 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
|
||||
id: 'unit-price',
|
||||
name: 'unitPrice',
|
||||
title: 'Unit Price',
|
||||
subtitle: 'Price of each tokens in collection',
|
||||
placeholder: '500',
|
||||
subtitle: 'Token price for whitelisted addresses \n (min. 25 STARS)',
|
||||
placeholder: '25',
|
||||
})
|
||||
|
||||
const memberLimitState = useNumberInputState({
|
||||
id: 'member-limit',
|
||||
name: 'memberLimit',
|
||||
title: 'Member Limit',
|
||||
subtitle: 'Limit of the whitelisted members',
|
||||
subtitle: 'Maximum number of whitelisted addresses',
|
||||
placeholder: '1000',
|
||||
})
|
||||
|
||||
@ -59,7 +59,7 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
|
||||
id: 'per-address-limit',
|
||||
name: 'perAddressLimit',
|
||||
title: 'Per Address Limit',
|
||||
subtitle: 'Limit of tokens per address',
|
||||
subtitle: 'Maximum number of tokens per whitelisted address',
|
||||
placeholder: '5',
|
||||
})
|
||||
|
||||
@ -145,11 +145,7 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
|
||||
</div>
|
||||
|
||||
<Conditional test={whitelistState === 'existing'}>
|
||||
<AddressInput
|
||||
{...whitelistAddressState}
|
||||
className="pb-5"
|
||||
onChange={(e) => whitelistAddressState.onChange(e.target.value)}
|
||||
/>
|
||||
<AddressInput {...whitelistAddressState} className="pb-5" isRequired />
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={whitelistState === 'new'}>
|
||||
@ -158,10 +154,20 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
|
||||
<NumberInput isRequired {...uniPriceState} />
|
||||
<NumberInput isRequired {...memberLimitState} />
|
||||
<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} />
|
||||
</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} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
@ -37,6 +37,7 @@ export interface MinterInstance {
|
||||
batchMint: (senderAddress: string, recipient: string, batchNumber: number) => Promise<string>
|
||||
shuffle: (senderAddress: string) => Promise<string>
|
||||
withdraw: (senderAddress: string) => Promise<string>
|
||||
airdrop: (senderAddress: string, recipients: string[]) => Promise<string>
|
||||
}
|
||||
|
||||
export interface MinterMessages {
|
||||
@ -46,9 +47,10 @@ export interface MinterMessages {
|
||||
updatePerAddressLimit: (perAddressLimit: number) => UpdatePerAddressLimitMessage
|
||||
mintTo: (recipient: string) => MintToMessage
|
||||
mintFor: (recipient: string, tokenId: number) => MintForMessage
|
||||
batchMint: (recipient: string, batchNumber: number) => BatchMintMessage
|
||||
batchMint: (recipient: string, batchNumber: number) => CustomMessage
|
||||
shuffle: () => ShuffleMessage
|
||||
withdraw: () => WithdrawMessage
|
||||
airdrop: (recipients: string[]) => CustomMessage
|
||||
}
|
||||
|
||||
export interface MintMessage {
|
||||
@ -114,7 +116,7 @@ export interface MintForMessage {
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface BatchMintMessage {
|
||||
export interface CustomMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: Record<string, unknown>[]
|
||||
@ -301,6 +303,29 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC
|
||||
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 res = await client.execute(
|
||||
senderAddress,
|
||||
@ -343,6 +368,7 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC
|
||||
mintTo,
|
||||
mintFor,
|
||||
batchMint,
|
||||
airdrop,
|
||||
shuffle,
|
||||
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>[] = []
|
||||
for (let i = 0; i < batchNumber; i++) {
|
||||
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 => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
@ -484,6 +523,7 @@ export const minter = (client: SigningCosmWasmClient, txSigner: string): MinterC
|
||||
mintTo,
|
||||
mintFor,
|
||||
batchMint,
|
||||
airdrop,
|
||||
shuffle,
|
||||
withdraw,
|
||||
}
|
||||
|
@ -22,6 +22,7 @@
|
||||
"@fontsource/roboto": "^4",
|
||||
"@headlessui/react": "^1",
|
||||
"@keplr-wallet/cosmos": "^0.9.16",
|
||||
"@pinata/sdk": "^1.1.26",
|
||||
"@popperjs/core": "^2",
|
||||
"@svgr/webpack": "^6",
|
||||
"@tailwindcss/forms": "^0",
|
||||
@ -34,7 +35,6 @@
|
||||
"next": "^12",
|
||||
"next-seo": "^4",
|
||||
"nft.storage": "^6.3.0",
|
||||
"@pinata/sdk": "^1.1.26",
|
||||
"react": "^18",
|
||||
"react-datetime-picker": "^3",
|
||||
"react-dom": "^18",
|
||||
@ -44,7 +44,6 @@
|
||||
"react-popper": "^2",
|
||||
"react-query": "^3",
|
||||
"react-tracked": "^1",
|
||||
"react-collapsed": "^3",
|
||||
"scheduler": "^0",
|
||||
"zustand": "^3"
|
||||
},
|
||||
|
@ -6,11 +6,13 @@ import { useActionsComboboxState } from 'components/collections/actions/Combobox
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { AddressInput, NumberInput } from 'components/forms/FormInput'
|
||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||
import { InputDateTime } from 'components/InputDateTime'
|
||||
import { JsonPreview } from 'components/JsonPreview'
|
||||
import { TransactionHash } from 'components/TransactionHash'
|
||||
import { WhitelistUpload } from 'components/WhitelistUpload'
|
||||
import { useContracts } from 'contexts/contracts'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { NextPage } from 'next'
|
||||
@ -31,6 +33,7 @@ const CollectionActionsPage: NextPage = () => {
|
||||
const [lastTx, setLastTx] = useState('')
|
||||
|
||||
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
||||
const [airdropArray, setAirdropArray] = useState<string[]>([])
|
||||
|
||||
const comboboxState = useActionsComboboxState()
|
||||
const type = comboboxState.value?.id
|
||||
@ -99,6 +102,7 @@ const CollectionActionsPage: NextPage = () => {
|
||||
const showNumberOfTokensField = type === 'batch_mint'
|
||||
const showTokenIdListField = isEitherType(type, ['batch_burn', 'batch_transfer'])
|
||||
const showRecipientField = isEitherType(type, ['transfer', 'mint_to', 'mint_for', 'batch_mint', 'batch_transfer'])
|
||||
const showAirdropFileField = type === 'airdrop'
|
||||
|
||||
const minterMessages = useMemo(
|
||||
() => minterContract?.use(minterContractState.value),
|
||||
@ -120,6 +124,7 @@ const CollectionActionsPage: NextPage = () => {
|
||||
minterMessages,
|
||||
sg721Messages,
|
||||
recipient: recipientState.value,
|
||||
recipients: airdropArray,
|
||||
txSigner: wallet.address,
|
||||
type,
|
||||
}
|
||||
@ -148,6 +153,10 @@ const CollectionActionsPage: NextPage = () => {
|
||||
},
|
||||
)
|
||||
|
||||
const airdropFileOnChange = (data: string[]) => {
|
||||
setAirdropArray(data)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="py-6 px-12 space-y-4">
|
||||
<NextSeo title="Collection Actions" />
|
||||
@ -168,6 +177,11 @@ const CollectionActionsPage: NextPage = () => {
|
||||
{showTokenIdField && <NumberInput {...tokenIdState} />}
|
||||
{showTokenIdListField && <TextInput {...tokenIdListState} />}
|
||||
{showNumberOfTokensField && <NumberInput {...batchNumberState} />}
|
||||
{showAirdropFileField && (
|
||||
<FormGroup subtitle="TXT file that contains the airdrop addresses" title="Airdrop File">
|
||||
<WhitelistUpload onChange={airdropFileOnChange} />
|
||||
</FormGroup>
|
||||
)}
|
||||
<Conditional test={showDateField}>
|
||||
<FormControl htmlId="start-date" subtitle="Start time for the minting" title="Start Time">
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
|
||||
|
@ -26,7 +26,6 @@ import { useWallet } from 'contexts/wallet'
|
||||
import type { NextPage } from 'next'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import useCollapse from 'react-collapsed'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { upload } from 'services/upload'
|
||||
import { compareFileArrays } from 'utils/compareFileArrays'
|
||||
@ -35,15 +34,12 @@ import { withMetadata } from 'utils/layout'
|
||||
import { links } from 'utils/links'
|
||||
|
||||
import type { UploadMethod } from '../../components/collections/creation/UploadDetails'
|
||||
import { ConfirmationModal } from '../../components/ConfirmationModal'
|
||||
import { getAssetType } from '../../utils/getAssetType'
|
||||
|
||||
const CollectionCreationPage: NextPage = () => {
|
||||
const wallet = useWallet()
|
||||
const { minter: minterContract, whitelist: whitelistContract } = useContracts()
|
||||
|
||||
const { getCollapseProps, getToggleProps, isExpanded } = useCollapse()
|
||||
const toggleProps = getToggleProps()
|
||||
const collapseProps = getCollapseProps()
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [uploadDetails, setUploadDetails] = useState<UploadDetailsDataProps | null>(null)
|
||||
@ -53,12 +49,29 @@ const CollectionCreationPage: NextPage = () => {
|
||||
const [royaltyDetails, setRoyaltyDetails] = useState<RoyaltyDetailsDataProps | null>(null)
|
||||
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [readyToCreate, setReadyToCreate] = useState(false)
|
||||
const [minterContractAddress, setMinterContractAddress] = useState<string | null>(null)
|
||||
const [sg721ContractAddress, setSg721ContractAddress] = useState<string | null>(null)
|
||||
const [baseTokenUri, setBaseTokenUri] = useState<string | null>(null)
|
||||
const [coverImageUrl, setCoverImageUrl] = 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 () => {
|
||||
try {
|
||||
setBaseTokenUri(null)
|
||||
@ -66,11 +79,6 @@ const CollectionCreationPage: NextPage = () => {
|
||||
setMinterContractAddress(null)
|
||||
setSg721ContractAddress(null)
|
||||
setTransactionHash(null)
|
||||
checkUploadDetails()
|
||||
checkCollectionDetails()
|
||||
checkMintingDetails()
|
||||
checkWhitelistDetails()
|
||||
checkRoyaltyDetails()
|
||||
if (uploadDetails?.uploadMethod === 'new') {
|
||||
setUploading(true)
|
||||
|
||||
@ -84,22 +92,27 @@ const CollectionCreationPage: NextPage = () => {
|
||||
uploadDetails.pinataApiKey as string,
|
||||
uploadDetails.pinataSecretKey as string,
|
||||
)
|
||||
|
||||
setUploading(false)
|
||||
|
||||
setBaseTokenUri(baseUri)
|
||||
setCoverImageUrl(coverImageUri)
|
||||
|
||||
let whitelist: string | undefined
|
||||
if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress
|
||||
else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist()
|
||||
|
||||
await instantiate(baseUri, coverImageUri, whitelist)
|
||||
} else {
|
||||
setBaseTokenUri(uploadDetails?.baseTokenURI as string)
|
||||
setCoverImageUrl(uploadDetails?.imageUrl as string)
|
||||
|
||||
let whitelist: string | undefined
|
||||
if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress
|
||||
else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist()
|
||||
|
||||
await instantiate(baseTokenUri as string, coverImageUrl as string, whitelist)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
toast.error(error.message)
|
||||
@ -141,13 +154,14 @@ const CollectionCreationPage: NextPage = () => {
|
||||
share: (Number(royaltyDetails.share) / 100).toString(),
|
||||
}
|
||||
}
|
||||
|
||||
const msg = {
|
||||
base_token_uri: `${uploadDetails?.uploadMethod === 'new' ? `ipfs://${baseUri}/` : `${baseUri}`}`,
|
||||
num_tokens: mintingDetails?.numTokens,
|
||||
sg721_code_id: SG721_CODE_ID,
|
||||
sg721_instantiate_msg: {
|
||||
name: collectionDetails?.name,
|
||||
symbol: 'SYMBOL',
|
||||
symbol: collectionDetails?.symbol,
|
||||
minter: wallet.address,
|
||||
collection_info: {
|
||||
creator: wallet.address,
|
||||
@ -187,20 +201,25 @@ const CollectionCreationPage: NextPage = () => {
|
||||
.then((assetUri: string) => {
|
||||
const fileArray: File[] = []
|
||||
let reader: FileReader
|
||||
|
||||
for (let i = 0; i < uploadDetails.metadataFiles.length; i++) {
|
||||
reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const data: any = JSON.parse(e.target?.result as string)
|
||||
|
||||
if (
|
||||
getAssetType(uploadDetails.assetFiles[i].name) === 'audio' ||
|
||||
getAssetType(uploadDetails.assetFiles[i].name) === 'video'
|
||||
) {
|
||||
data.animation_url = `ipfs://${assetUri}/${uploadDetails.assetFiles[i].name}`
|
||||
}
|
||||
|
||||
data.image = `ipfs://${assetUri}/${uploadDetails.assetFiles[i].name}`
|
||||
|
||||
const metadataFileBlob = new Blob([JSON.stringify(data)], {
|
||||
type: 'application/json',
|
||||
})
|
||||
|
||||
const updatedMetadataFile = new File(
|
||||
[metadataFileBlob],
|
||||
uploadDetails.metadataFiles[i].name.substring(0, uploadDetails.metadataFiles[i].name.lastIndexOf('.')),
|
||||
@ -208,6 +227,7 @@ const CollectionCreationPage: NextPage = () => {
|
||||
type: 'application/json',
|
||||
},
|
||||
)
|
||||
|
||||
fileArray.push(updatedMetadataFile)
|
||||
}
|
||||
reader.onloadend = () => {
|
||||
@ -286,6 +306,7 @@ const CollectionCreationPage: NextPage = () => {
|
||||
)
|
||||
throw new Error('Invalid limit for tokens per address')
|
||||
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 = () => {
|
||||
@ -301,14 +322,18 @@ const CollectionCreationPage: NextPage = () => {
|
||||
if (whitelistDetails.endTime === '') throw new Error('End time 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 (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 = () => {
|
||||
if (!royaltyDetails) throw new Error('Please fill out the royalty details')
|
||||
if (royaltyDetails.royaltyType === 'new') {
|
||||
if (royaltyDetails.share === 0) throw new Error('Royalty share is required')
|
||||
if (royaltyDetails.share > 100 || royaltyDetails.share < 0) throw new Error('Invalid royalty share')
|
||||
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')
|
||||
}
|
||||
}
|
||||
@ -411,21 +436,22 @@ const CollectionCreationPage: NextPage = () => {
|
||||
uploadMethod={uploadDetails?.uploadMethod as UploadMethod}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<div className="my-6">
|
||||
<WhitelistDetails onChange={setWhitelistDetails} />
|
||||
<div className="my-6" />
|
||||
<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>
|
||||
)
|
||||
|
22
yarn.lock
22
yarn.lock
@ -6541,11 +6541,6 @@ pbkdf2@^3.0.16, pbkdf2@^3.0.9, pbkdf2@^3.1.1:
|
||||
safe-buffer "^5.0.1"
|
||||
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:
|
||||
version "1.0.0"
|
||||
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"
|
||||
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:
|
||||
version "2.1.0"
|
||||
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"
|
||||
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:
|
||||
version "8.4.0"
|
||||
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"
|
||||
nan "^2.13.2"
|
||||
|
||||
tiny-warning@^1.0.0, tiny-warning@^1.0.3:
|
||||
tiny-warning@^1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz"
|
||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||
|
Loading…
Reference in New Issue
Block a user