diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 399dcae..b214988 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -46,7 +46,7 @@ export const Sidebar = () => { !router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1).includes(href) && isChild, }, { - 'text-plumbus': router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1).includes(href) && isChild, + 'text-stargaze': router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1).includes(href) && isChild, }, // active route styling // { 'text-gray-500 pointer-events-none': disabled }, // disabled route styling )} diff --git a/components/collections/creation/MinterDetails.tsx b/components/collections/creation/MinterDetails.tsx new file mode 100644 index 0000000..3805085 --- /dev/null +++ b/components/collections/creation/MinterDetails.tsx @@ -0,0 +1,199 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { toUtf8 } from '@cosmjs/encoding' +import axios from 'axios' +import { useInputState } from 'components/forms/FormInput.hooks' +import { useWallet } from 'contexts/wallet' +import React, { useCallback, useEffect, useState } from 'react' +import { toast } from 'react-hot-toast' +import { API_URL } from 'utils/constants' + +import { useDebounce } from '../../../utils/debounce' +import { TextInput } from '../../forms/FormInput' +import type { MinterType } from '../actions/Combobox' + +export type MinterAcquisitionMethod = 'existing' | 'new' + +export interface MinterInfo { + name: string + minter: string +} + +interface MinterDetailsProps { + onChange: (data: MinterDetailsDataProps) => void + minterType: MinterType +} + +export interface MinterDetailsDataProps { + minterAcquisitionMethod: MinterAcquisitionMethod + existingMinter: string | undefined +} + +export const MinterDetails = ({ onChange, minterType }: MinterDetailsProps) => { + const wallet = useWallet() + + const [myBaseMinterContracts, setMyBaseMinterContracts] = useState([]) + const [minterAcquisitionMethod, setMinterAcquisitionMethod] = useState('existing') + + const existingMinterState = useInputState({ + id: 'existingMinter', + name: 'existingMinter', + title: 'Existing Base Minter Contract Address', + subtitle: '', + placeholder: 'stars1...', + }) + + const fetchMinterContracts = async (): Promise => { + const contracts: MinterInfo[] = await axios + .get(`${API_URL}/api/v1beta/collections/${wallet.address}`) + .then((response) => { + const collectionData = response.data + const minterContracts = collectionData.map((collection: any) => { + return { name: collection.name, minter: collection.minter } + }) + return minterContracts + }) + .catch(console.error) + console.log(contracts) + return contracts + } + + async function getMinterContractType(minterContractAddress: string) { + if (wallet.client && minterContractAddress.length > 0) { + const client = wallet.client + const data = await client.queryContractRaw( + minterContractAddress, + toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()), + ) + const contractType: string = JSON.parse(new TextDecoder().decode(data as Uint8Array)).contract + return contractType + } + } + + const filterBaseMinterContracts = async () => { + setMyBaseMinterContracts([]) + await fetchMinterContracts() + .then((minterContracts) => + minterContracts.map(async (minterContract: any) => { + await getMinterContractType(minterContract.minter) + .then((contractType) => { + if (contractType?.includes('sg-minter')) { + setMyBaseMinterContracts((prevState) => [...prevState, minterContract]) + } + }) + .catch((err) => { + console.log(err) + console.log('Unable to retrieve contract type') + }) + }), + ) + .catch((err) => { + console.log(err) + console.log('Unable to fetch base minter contracts') + }) + } + + const debouncedMyBaseMinterContracts = useDebounce(myBaseMinterContracts, 500) + + const renderMinterContracts = useCallback(() => { + return myBaseMinterContracts.map((minterContract, index) => { + return ( + + ) + }) + }, [debouncedMyBaseMinterContracts]) + + const debouncedWalletAddress = useDebounce(wallet.address, 500) + + const displayToast = async () => { + await toast.promise(filterBaseMinterContracts(), { + loading: 'Fetching Base Minter contracts...', + success: 'Base Minter contracts retrieved.', + error: 'Unable to retrieve Base Minter contracts.', + }) + } + + useEffect(() => { + if (debouncedWalletAddress && minterAcquisitionMethod === 'existing') { + void displayToast() + } + }, [debouncedWalletAddress, minterAcquisitionMethod]) + + useEffect(() => { + const data: MinterDetailsDataProps = { + minterAcquisitionMethod, + existingMinter: existingMinterState.value, + } + onChange(data) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [existingMinterState.value, minterAcquisitionMethod]) + + return ( +
+
+
+ { + setMinterAcquisitionMethod('new') + }} + type="radio" + value="New" + /> + +
+
+ { + setMinterAcquisitionMethod('existing') + }} + type="radio" + value="Existing" + /> + +
+
+ + {minterAcquisitionMethod === 'existing' && ( +
+
+ + +
+
+ )} +
+ ) +} diff --git a/components/collections/creation/UploadDetails.tsx b/components/collections/creation/UploadDetails.tsx index fbf417f..9ee9261 100644 --- a/components/collections/creation/UploadDetails.tsx +++ b/components/collections/creation/UploadDetails.tsx @@ -233,7 +233,7 @@ export const UploadDetails = ({ onChange }: UploadDetailsProps) => { }, [uploadMethod]) return ( -
+
{ const [uploadDetails, setUploadDetails] = useState(null) const [collectionDetails, setCollectionDetails] = useState(null) + const [minterDetails, setMinterDetails] = useState(null) const [mintingDetails, setMintingDetails] = useState(null) const [whitelistDetails, setWhitelistDetails] = useState(null) const [royaltyDetails, setRoyaltyDetails] = useState(null) + const [minterType, setMinterType] = useState('vending') const [uploading, setUploading] = useState(false) const [creatingCollection, setCreatingCollection] = useState(false) - const [readyToCreate, setReadyToCreate] = useState(false) + const [readyToCreateVm, setReadyToCreateVm] = useState(false) + const [readyToCreateBm, setReadyToCreateBm] = useState(false) + const [readyToUploadAndMint, setReadyToUploadAndMint] = useState(false) const [vendingMinterContractAddress, setVendingMinterContractAddress] = useState(null) const [sg721ContractAddress, setSg721ContractAddress] = useState(null) const [whitelistContractAddress, setWhitelistContractAddress] = useState(null) @@ -76,20 +84,20 @@ const CollectionCreationPage: NextPage = () => { const [coverImageUrl, setCoverImageUrl] = useState(null) const [transactionHash, setTransactionHash] = useState(null) - const performChecks = () => { + const performVendingMinterChecks = () => { try { - setReadyToCreate(false) + setReadyToCreateVm(false) checkUploadDetails() checkCollectionDetails() checkMintingDetails() checkRoyaltyDetails() checkWhitelistDetails() .then(() => { - setReadyToCreate(true) + setReadyToCreateVm(true) }) .catch((err) => { toast.error(`Error in Whitelist Configuration: ${err.message}`, { style: { maxWidth: 'none' } }) - setReadyToCreate(false) + setReadyToCreateVm(false) }) } catch (error: any) { toast.error(error.message, { style: { maxWidth: 'none' } }) @@ -97,7 +105,157 @@ const CollectionCreationPage: NextPage = () => { } } - const createCollection = async () => { + const performBaseMinterChecks = () => { + try { + setReadyToCreateBm(false) + checkUploadDetails() + checkCollectionDetails() + checkMintingDetails() + checkRoyaltyDetails() + checkWhitelistDetails() + .then(() => { + setReadyToCreateBm(true) + }) + .catch((err) => { + toast.error(`Error in Whitelist Configuration: ${err.message}`, { style: { maxWidth: 'none' } }) + setReadyToCreateBm(false) + }) + } catch (error: any) { + toast.error(error.message, { style: { maxWidth: 'none' } }) + setUploading(false) + } + } + + const performUploadAndMintChecks = () => { + try { + setReadyToUploadAndMint(false) + checkUploadDetails() + checkCollectionDetails() + checkMintingDetails() + checkRoyaltyDetails() + checkWhitelistDetails() + .then(() => { + setReadyToUploadAndMint(true) + }) + .catch((err) => { + toast.error(`Error in Whitelist Configuration: ${err.message}`, { style: { maxWidth: 'none' } }) + setReadyToUploadAndMint(false) + }) + } catch (error: any) { + toast.error(error.message, { style: { maxWidth: 'none' } }) + setUploading(false) + } + } + + const createVendingMinterCollection = async () => { + try { + setCreatingCollection(true) + setBaseTokenUri(null) + setCoverImageUrl(null) + setVendingMinterContractAddress(null) + setSg721ContractAddress(null) + setWhitelistContractAddress(null) + setTransactionHash(null) + if (uploadDetails?.uploadMethod === 'new') { + setUploading(true) + + const baseUri = await uploadFiles() + //upload coverImageUri and append the file name + const coverImageUri = await upload( + collectionDetails?.imageFile as File[], + uploadDetails.uploadService, + 'cover', + uploadDetails.nftStorageApiKey as string, + uploadDetails.pinataApiKey as string, + uploadDetails.pinataSecretKey as string, + ) + + setUploading(false) + + setBaseTokenUri(baseUri) + setCoverImageUrl(coverImageUri) + + let whitelist: string | undefined + if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress + else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() + setWhitelistContractAddress(whitelist as string) + + await instantiate(baseUri, coverImageUri, whitelist) + } else { + 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() + setWhitelistContractAddress(whitelist as string) + + await instantiate(baseTokenUri as string, coverImageUrl as string, whitelist) + } + setCreatingCollection(false) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + toast.error(error.message, { style: { maxWidth: 'none' } }) + setCreatingCollection(false) + setUploading(false) + } + } + + const createBaseMinterCollection = async () => { + try { + setCreatingCollection(true) + setBaseTokenUri(null) + setCoverImageUrl(null) + setVendingMinterContractAddress(null) + setSg721ContractAddress(null) + setWhitelistContractAddress(null) + setTransactionHash(null) + if (uploadDetails?.uploadMethod === 'new') { + setUploading(true) + + const baseUri = await uploadFiles() + //upload coverImageUri and append the file name + const coverImageUri = await upload( + collectionDetails?.imageFile as File[], + uploadDetails.uploadService, + 'cover', + uploadDetails.nftStorageApiKey as string, + uploadDetails.pinataApiKey as string, + uploadDetails.pinataSecretKey as string, + ) + + setUploading(false) + + setBaseTokenUri(baseUri) + setCoverImageUrl(coverImageUri) + + let whitelist: string | undefined + if (whitelistDetails?.whitelistType === 'existing') whitelist = whitelistDetails.contractAddress + else if (whitelistDetails?.whitelistType === 'new') whitelist = await instantiateWhitelist() + setWhitelistContractAddress(whitelist as string) + + await instantiate(baseUri, coverImageUri, whitelist) + } else { + 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() + setWhitelistContractAddress(whitelist as string) + + await instantiate(baseTokenUri as string, coverImageUrl as string, whitelist) + } + setCreatingCollection(false) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + toast.error(error.message, { style: { maxWidth: 'none' } }) + setCreatingCollection(false) + setUploading(false) + } + } + + const uploadAndMint = async () => { try { setCreatingCollection(true) setBaseTokenUri(null) @@ -513,36 +671,142 @@ const CollectionCreationPage: NextPage = () => {
+
+
+
+ +
+
+ +
+
+
+ + {minterType === 'base' && ( +
+ +
+ )} +
-
- - -
-
- -
- -
- {readyToCreate && } + +
+ + + + + + +
+
+ + +
+ + +
+ + +
+
+ + + + + + + + +
- + + + + + + + + +
diff --git a/yarn.lock b/yarn.lock index 8034166..da8a445 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3704,6 +3704,11 @@ clsx@^1: resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== +clsx@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" @@ -4922,10 +4927,10 @@ globby@^11.0.4: merge2 "^1.4.1" slash "^3.0.0" -goober@^2.1.1: - version "2.1.9" - resolved "https://registry.npmjs.org/goober/-/goober-2.1.9.tgz" - integrity sha512-PAtnJbrWtHbfpJUIveG5PJIB6Mc9Kd0gimu9wZwPyA+wQUSeOeA4x4Ug16lyaaUUKZ/G6QEH1xunKOuXP1F4Vw== +goober@^2.1.10: + version "2.1.11" + resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.11.tgz#bbd71f90d2df725397340f808dbe7acc3118e610" + integrity sha512-5SS2lmxbhqH0u9ABEWq7WPU69a4i2pYcHeCxqaNq6Cw3mnrF0ghWNM4tEGid4dKy8XNIAUbuThuozDHHKJVh3A== hamt-sharding@^2.0.0: version "2.0.1" @@ -6806,12 +6811,12 @@ react-hook-form@^7: resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.30.0.tgz" integrity sha512-DzjiM6o2vtDGNMB9I4yCqW8J21P314SboNG1O0obROkbg7KVS0I7bMtwSdKyapnCPjHgnxc3L7E5PEdISeEUcQ== -react-hot-toast@^2: - version "2.2.0" - resolved "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.2.0.tgz" - integrity sha512-248rXw13uhf/6TNDVzagX+y7R8J183rp7MwUMNkcrBRyHj/jWOggfXTGlM8zAOuh701WyVW+eUaWG2LeSufX9g== +react-hot-toast@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.4.0.tgz#b91e7a4c1b6e3068fc599d3d83b4fb48668ae51d" + integrity sha512-qnnVbXropKuwUpriVVosgo8QrB+IaPJCpL8oBI6Ov84uvHZ5QQcTp2qg6ku2wNfgJl6rlQXJIQU5q+5lmPOutA== dependencies: - goober "^2.1.1" + goober "^2.1.10" react-icons@^4: version "4.3.1" @@ -6862,6 +6867,13 @@ react-time-picker@^4.5.0: react-fit "^1.4.0" update-input-width "^1.2.2" +react-toastify@9.1.1: + version "9.1.1" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.1.1.tgz#9280caea4a13dc1739c350d90660a630807bf10b" + integrity sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw== + dependencies: + clsx "^1.1.1" + react-tracked@^1: version "1.7.9" resolved "https://registry.npmjs.org/react-tracked/-/react-tracked-1.7.9.tgz"