Merge pull request #195 from public-awesome/ibc-minter-creation

IBC minter support
This commit is contained in:
Serkan Reis
2023-08-17 16:30:28 +03:00
committed by GitHub
14 changed files with 655 additions and 103 deletions
@@ -4,6 +4,9 @@ import { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime'
import { vendingMinterList } from 'config/minter'
import type { TokenInfo } from 'config/token'
import { stars, tokensList } from 'config/token'
import React, { useEffect, useState } from 'react'
import { resolveAddress } from 'utils/resolveAddress'
@@ -16,6 +19,7 @@ interface MintingDetailsProps {
numberOfTokens: number | undefined
uploadMethod: UploadMethod
minimumMintPrice: number
mintingTokenFromFactory?: TokenInfo
}
export interface MintingDetailsDataProps {
@@ -24,12 +28,20 @@ export interface MintingDetailsDataProps {
perAddressLimit: number
startTime: string
paymentAddress?: string
selectedMintToken?: TokenInfo
}
export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod, minimumMintPrice }: MintingDetailsProps) => {
export const MintingDetails = ({
onChange,
numberOfTokens,
uploadMethod,
minimumMintPrice,
mintingTokenFromFactory,
}: MintingDetailsProps) => {
const wallet = useWallet()
const [timestamp, setTimestamp] = useState<Date | undefined>()
const [selectedMintToken, setSelectedMintToken] = useState<TokenInfo | undefined>(stars)
const numberOfTokensState = useNumberInputState({
id: 'numberoftokens',
@@ -43,7 +55,9 @@ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod, minimum
id: 'unitPrice',
name: 'unitPrice',
title: 'Unit Price',
subtitle: `Price of each token (min. ${minimumMintPrice} STARS)`,
subtitle: `Price of each token (min. ${minimumMintPrice} ${
mintingTokenFromFactory ? mintingTokenFromFactory.displayName : 'STARS'
})`,
placeholder: '50',
})
@@ -85,6 +99,7 @@ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod, minimum
perAddressLimit: perAddressLimitState.value,
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
paymentAddress: paymentAddressState.value.trim(),
selectedMintToken,
}
onChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -95,6 +110,7 @@ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod, minimum
perAddressLimitState.value,
timestamp,
paymentAddressState.value,
selectedMintToken,
])
return (
@@ -106,7 +122,22 @@ export const MintingDetails = ({ onChange, numberOfTokens, uploadMethod, minimum
isRequired
value={uploadMethod === 'new' ? numberOfTokens : numberOfTokensState.value}
/>
<NumberInput {...unitPriceState} isRequired />
<div className="flex flex-row items-center">
<NumberInput {...unitPriceState} isRequired />
<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))}
>
{vendingMinterList
.filter((minter) => minter.factoryAddress !== undefined && minter.updatable === false)
.map((minter) => (
<option key={minter.id} className="bg-black" value={minter.supportedToken.displayName}>
{minter.supportedToken.displayName}
</option>
))}
</select>
</div>
<NumberInput {...perAddressLimitState} isRequired />
<FormControl htmlId="timestamp" isRequired subtitle="Minting start time (local)" title="Start Time">
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
@@ -8,6 +8,7 @@ import { useInputState, useNumberInputState } from 'components/forms/FormInput.h
import { InputDateTime } from 'components/InputDateTime'
import type { WhitelistFlexMember } from 'components/WhitelistFlexUpload'
import { WhitelistFlexUpload } from 'components/WhitelistFlexUpload'
import type { TokenInfo } from 'config/token'
import React, { useEffect, useState } from 'react'
import { isValidAddress } from 'utils/isValidAddress'
@@ -18,6 +19,7 @@ import { WhitelistUpload } from '../../WhitelistUpload'
interface WhitelistDetailsProps {
onChange: (data: WhitelistDetailsDataProps) => void
mintingTokenFromFactory?: TokenInfo
}
export interface WhitelistDetailsDataProps {
@@ -38,7 +40,7 @@ type WhitelistState = 'none' | 'existing' | 'new'
type WhitelistType = 'standard' | 'flex'
export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
export const WhitelistDetails = ({ onChange, mintingTokenFromFactory }: WhitelistDetailsProps) => {
const [whitelistState, setWhitelistState] = useState<WhitelistState>('none')
const [whitelistType, setWhitelistType] = useState<WhitelistType>('standard')
const [startDate, setStartDate] = useState<Date | undefined>(undefined)
@@ -58,7 +60,9 @@ export const WhitelistDetails = ({ onChange }: WhitelistDetailsProps) => {
id: 'unit-price',
name: 'unitPrice',
title: 'Unit Price',
subtitle: 'Token price for whitelisted addresses \n (min. 0 STARS)',
subtitle: `Token price for whitelisted addresses \n (min. 0 ${
mintingTokenFromFactory ? mintingTokenFromFactory.displayName : 'STARS'
})`,
placeholder: '25',
})
+41 -5
View File
@@ -4,6 +4,9 @@ import { FormControl } from 'components/FormControl'
import { FormGroup } from 'components/FormGroup'
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
import { InputDateTime } from 'components/InputDateTime'
import { openEditionMinterList } from 'config/minter'
import type { TokenInfo } from 'config/token'
import { stars, tokensList } from 'config/token'
import React, { useEffect, useState } from 'react'
import { resolveAddress } from 'utils/resolveAddress'
@@ -15,6 +18,7 @@ interface MintingDetailsProps {
onChange: (data: MintingDetailsDataProps) => void
uploadMethod: UploadMethod
minimumMintPrice: number
mintTokenFromFactory?: TokenInfo | undefined
}
export interface MintingDetailsDataProps {
@@ -23,19 +27,28 @@ export interface MintingDetailsDataProps {
startTime: string
endTime: string
paymentAddress?: string
selectedMintToken?: TokenInfo
}
export const MintingDetails = ({ onChange, uploadMethod, minimumMintPrice }: MintingDetailsProps) => {
export const MintingDetails = ({
onChange,
uploadMethod,
minimumMintPrice,
mintTokenFromFactory,
}: MintingDetailsProps) => {
const wallet = useWallet()
const [timestamp, setTimestamp] = useState<Date | undefined>()
const [endTimestamp, setEndTimestamp] = useState<Date | undefined>()
const [selectedMintToken, setSelectedMintToken] = useState<TokenInfo | undefined>(stars)
const unitPriceState = useNumberInputState({
id: 'unitPrice',
name: 'unitPrice',
title: 'Unit Price',
subtitle: `Price of each token (min. ${minimumMintPrice} STARS)`,
title: 'Mint Price',
subtitle: `Price of each token (min. ${minimumMintPrice} ${
mintTokenFromFactory ? mintTokenFromFactory.displayName : 'STARS'
})`,
placeholder: '50',
})
@@ -76,15 +89,38 @@ export const MintingDetails = ({ onChange, uploadMethod, minimumMintPrice }: Min
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
endTime: endTimestamp ? (endTimestamp.getTime() * 1_000_000).toString() : '',
paymentAddress: paymentAddressState.value.trim(),
selectedMintToken,
}
onChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [unitPriceState.value, perAddressLimitState.value, timestamp, endTimestamp, paymentAddressState.value])
}, [
unitPriceState.value,
perAddressLimitState.value,
timestamp,
endTimestamp,
paymentAddressState.value,
selectedMintToken,
])
return (
<div className="border-l-[1px] border-gray-500 border-opacity-20">
<FormGroup subtitle="Information about your minting settings" title="Minting Details">
<NumberInput {...unitPriceState} isRequired />
<div className="flex flex-row items-center">
<NumberInput {...unitPriceState} isRequired />
<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))}
>
{openEditionMinterList
.filter((minter) => minter.factoryAddress !== undefined && minter.updatable === false)
.map((minter) => (
<option key={minter.id} className="bg-black" value={minter.supportedToken.displayName}>
{minter.supportedToken.displayName}
</option>
))}
</select>
</div>
<NumberInput {...perAddressLimitState} isRequired />
<FormControl htmlId="timestamp" isRequired subtitle="Minting start time (local)" title="Start Time">
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
@@ -12,6 +12,8 @@ import type { MinterType } from 'components/collections/actions/Combobox'
import { Conditional } from 'components/Conditional'
import { ConfirmationModal } from 'components/ConfirmationModal'
import { LoadingModal } from 'components/LoadingModal'
import { openEditionMinterList } from 'config/minter'
import type { TokenInfo } from 'config/token'
import { useContracts } from 'contexts/contracts'
import { addLogItem } from 'contexts/log'
import { useWallet } from 'contexts/wallet'
@@ -47,13 +49,25 @@ import { type RoyaltyDetailsDataProps, RoyaltyDetails } from './RoyaltyDetails'
export type MetadataStorageMethod = 'off-chain' | 'on-chain'
export interface OpenEditionMinterDetailsDataProps {
imageUploadDetails?: ImageUploadDetailsDataProps
collectionDetails?: CollectionDetailsDataProps
royaltyDetails?: RoyaltyDetailsDataProps
onChainMetadataInputDetails?: OnChainMetadataInputDetailsDataProps
offChainMetadataUploadDetails?: OffChainMetadataUploadDetailsDataProps
mintingDetails?: MintingDetailsDataProps
metadataStorageMethod?: MetadataStorageMethod
}
interface OpenEditionMinterCreatorProps {
onChange: (data: OpenEditionMinterCreatorDataProps) => void
onDetailsChange: (data: OpenEditionMinterDetailsDataProps) => void
openEditionMinterUpdatableCreationFee?: string
openEditionMinterCreationFee?: string
minimumMintPrice?: string
minimumUpdatableMintPrice?: string
minterType?: MinterType
mintTokenFromFactory?: TokenInfo | undefined
}
export interface OpenEditionMinterCreatorDataProps {
@@ -65,20 +79,18 @@ export interface OpenEditionMinterCreatorDataProps {
export const OpenEditionMinterCreator = ({
onChange,
onDetailsChange,
openEditionMinterCreationFee,
openEditionMinterUpdatableCreationFee,
minimumMintPrice,
minimumUpdatableMintPrice,
minterType,
mintTokenFromFactory,
}: OpenEditionMinterCreatorProps) => {
const wallet = useWallet()
const { openEditionMinter: openEditionMinterContract, openEditionFactory: openEditionFactoryContract } =
useContracts()
const openEditionFactoryMessages = useMemo(
() => openEditionFactoryContract?.use(OPEN_EDITION_FACTORY_ADDRESS),
[openEditionFactoryContract, wallet.address],
)
const [metadataStorageMethod, setMetadataStorageMethod] = useState<MetadataStorageMethod>('off-chain')
const [imageUploadDetails, setImageUploadDetails] = useState<ImageUploadDetailsDataProps | null>(null)
const [collectionDetails, setCollectionDetails] = useState<CollectionDetailsDataProps | null>(null)
@@ -99,6 +111,21 @@ export const OpenEditionMinterCreator = ({
const [sg721ContractAddress, setSg721ContractAddress] = useState<string | null>(null)
const [transactionHash, setTransactionHash] = useState<string | null>(null)
const factoryAddressForSelectedDenom =
openEditionMinterList.find((minter) => minter.supportedToken === mintTokenFromFactory && minter.updatable === false)
?.factoryAddress || OPEN_EDITION_FACTORY_ADDRESS
const updatableFactoryAddressForSelectedDenom =
openEditionMinterList.find((minter) => minter.supportedToken === mintTokenFromFactory && minter.updatable === true)
?.factoryAddress || OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS
const openEditionFactoryMessages = useMemo(
() =>
openEditionFactoryContract?.use(
collectionDetails?.updatable ? updatableFactoryAddressForSelectedDenom : factoryAddressForSelectedDenom,
),
[openEditionFactoryContract, wallet.address],
)
const performOpenEditionMinterChecks = () => {
try {
setReadyToCreate(false)
@@ -257,10 +284,16 @@ export const OpenEditionMinterCreator = ({
if (collectionDetails?.updatable) {
if (Number(mintingDetails.unitPrice) < Number(minimumUpdatableMintPrice))
throw new Error(
`Invalid mint price: The minimum mint price is ${Number(minimumUpdatableMintPrice) / 1000000} STARS`,
`Invalid mint price: The minimum mint price is ${Number(minimumUpdatableMintPrice) / 1000000} ${
mintTokenFromFactory?.displayName
}`,
)
} else if (Number(mintingDetails.unitPrice) < Number(minimumMintPrice))
throw new Error(`Invalid mint price: The minimum mint price is ${Number(minimumMintPrice) / 1000000} STARS`)
throw new Error(
`Invalid mint price: The minimum mint price is ${Number(minimumMintPrice) / 1000000} ${
mintTokenFromFactory?.displayName
}`,
)
if (!mintingDetails.perAddressLimit || mintingDetails.perAddressLimit < 1 || mintingDetails.perAddressLimit > 50)
throw new Error('Invalid limit for tokens per address')
if (mintingDetails.startTime === '') throw new Error('Start time is required')
@@ -515,7 +548,7 @@ export const OpenEditionMinterCreator = ({
end_time: mintingDetails?.endTime,
mint_price: {
amount: Number(mintingDetails?.unitPrice).toString(),
denom: 'ustars',
denom: (mintTokenFromFactory?.denom as string) || 'ustars',
},
per_address_limit: mintingDetails?.perAddressLimit,
payment_address: mintingDetails?.paymentAddress || null,
@@ -537,9 +570,9 @@ export const OpenEditionMinterCreator = ({
}
console.log('msg: ', msg)
console.log('Using factory address: ', factoryAddressForSelectedDenom)
const payload: OpenEditionFactoryDispatchExecuteArgs = {
contract: collectionDetails?.updatable ? OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS : OPEN_EDITION_FACTORY_ADDRESS,
contract: collectionDetails?.updatable ? updatableFactoryAddressForSelectedDenom : factoryAddressForSelectedDenom,
messages: openEditionFactoryMessages,
txSigner: wallet.address,
msg,
@@ -587,6 +620,27 @@ export const OpenEditionMinterCreator = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [metadataStorageMethod, openEditionMinterContractAddress, sg721ContractAddress, transactionHash])
useEffect(() => {
const data: OpenEditionMinterDetailsDataProps = {
imageUploadDetails: imageUploadDetails ? imageUploadDetails : undefined,
collectionDetails: collectionDetails ? collectionDetails : undefined,
royaltyDetails: royaltyDetails ? royaltyDetails : undefined,
onChainMetadataInputDetails: onChainMetadataInputDetails ? onChainMetadataInputDetails : undefined,
offChainMetadataUploadDetails: offChainMetadataUploadDetails ? offChainMetadataUploadDetails : undefined,
mintingDetails: mintingDetails ? mintingDetails : undefined,
metadataStorageMethod,
}
onDetailsChange(data)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
imageUploadDetails,
collectionDetails,
royaltyDetails,
onChainMetadataInputDetails,
offChainMetadataUploadDetails,
mintingDetails,
])
return (
<div>
{/* TODO: Cancel once we're able to index on-chain metadata */}
@@ -671,6 +725,7 @@ export const OpenEditionMinterCreator = ({
? Number(minimumUpdatableMintPrice) / 1000000
: Number(minimumMintPrice) / 1000000
}
mintTokenFromFactory={mintTokenFromFactory}
onChange={setMintingDetails}
uploadMethod={offChainMetadataUploadDetails?.uploadMethod as UploadMethod}
/>