Merge pull request #171 from public-awesome/open-edition-minter
Open Edition minter integration
This commit is contained in:
commit
381c55dab3
@ -1,4 +1,4 @@
|
||||
APP_VERSION=0.6.4
|
||||
APP_VERSION=0.6.5
|
||||
|
||||
NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
|
||||
NEXT_PUBLIC_SG721_CODE_ID=2092
|
||||
@ -11,6 +11,9 @@ NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_ADDRESS="stars1fnfywcnzzwledr93at65qm8gf95
|
||||
NEXT_PUBLIC_VENDING_FACTORY_FLEX_ADDRESS="stars1gy6hr9sq9fzrykzw0emmehnjy27agreuepjrfnjnlwlugg29l2qqt0yu2j"
|
||||
NEXT_PUBLIC_BASE_FACTORY_ADDRESS="stars18kzfpdgx36m95mszchegnk7car4sq03uvg25zeph2j7xg3rk03cs007sxr"
|
||||
NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_ADDRESS="stars13pw8r33dsnghlxfj2upaywf38z2fc6npuw9maq9e5cpet4v285sscgzjp2"
|
||||
NEXT_PUBLIC_OPEN_EDITION_FACTORY_ADDRESS="stars1m4fjq0qf3mj6zhplng7yxngp9hq98hd4tj90gjp759l5e0fvf3qqkqzqsf"
|
||||
NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS="stars1ss39vpz3wfv76nkxpls48srvf58lk57980yatwcjrvvygv9nt8tq5rgslt"
|
||||
NEXT_PUBLIC_OPEN_EDITION_MINTER_CODE_ID=2570
|
||||
NEXT_PUBLIC_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr"
|
||||
NEXT_PUBLIC_WHITELIST_CODE_ID=2093
|
||||
NEXT_PUBLIC_WHITELIST_FLEX_CODE_ID=2005
|
||||
|
@ -41,6 +41,24 @@ export const vendingMinterLinkTabs: LinkTabProps[] = [
|
||||
},
|
||||
]
|
||||
|
||||
export const openEditionMinterLinkTabs: LinkTabProps[] = [
|
||||
{
|
||||
title: 'Query',
|
||||
description: `Dispatch queries for your Open Edition Minter contract`,
|
||||
href: '/contracts/openEditionMinter/query',
|
||||
},
|
||||
{
|
||||
title: 'Execute',
|
||||
description: `Execute Open Edition Minter contract actions`,
|
||||
href: '/contracts/openEditionMinter/execute',
|
||||
},
|
||||
{
|
||||
title: 'Migrate',
|
||||
description: `Migrate Open Edition Minter contract`,
|
||||
href: '/contracts/openEditionMinter/migrate',
|
||||
},
|
||||
]
|
||||
|
||||
export const baseMinterLinkTabs: LinkTabProps[] = [
|
||||
{
|
||||
title: 'Instantiate',
|
||||
|
@ -175,9 +175,9 @@ export const MetadataInput = (props: MetadataInputProps) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="grid grid-cols-2 mx-4 mt-4 w-full max-w-6xl max-h-full no-scrollbar">
|
||||
<div className="grid grid-cols-2 mt-4 mr-4 ml-8 w-full max-w-6xl max-h-full no-scrollbar">
|
||||
<div className="mr-4">
|
||||
<div className="mb-7 text-xl font-bold underline underline-offset-2">NFT Metadata</div>
|
||||
<div className="mb-7 text-xl font-bold underline underline-offset-4">NFT Metadata</div>
|
||||
<TextInput {...nameState} />
|
||||
<TextInput className="mt-2" {...descriptionState} />
|
||||
<TextInput className="mt-2" {...externalUrlState} />
|
||||
|
@ -11,7 +11,7 @@ import { useEffect } from 'react'
|
||||
// import BrandText from 'public/brand/brand-text.svg'
|
||||
import { footerLinks, socialsLinks } from 'utils/links'
|
||||
|
||||
import { BADGE_HUB_ADDRESS, BASE_FACTORY_ADDRESS, NETWORK } from '../utils/constants'
|
||||
import { BADGE_HUB_ADDRESS, BASE_FACTORY_ADDRESS, NETWORK, OPEN_EDITION_FACTORY_ADDRESS } from '../utils/constants'
|
||||
import { Conditional } from './Conditional'
|
||||
import { IncomeDashboardDisclaimer } from './IncomeDashboardDisclaimer'
|
||||
import { LogModal } from './LogModal'
|
||||
@ -175,6 +175,17 @@ export const Sidebar = () => {
|
||||
>
|
||||
<Link href="/contracts/vendingMinter/">Vending Minter Contract</Link>
|
||||
</li>
|
||||
<Conditional test={OPEN_EDITION_FACTORY_ADDRESS !== undefined}>
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
router.asPath.includes('/contracts/openEditionMinter/') ? 'text-white' : 'text-gray',
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Link href="/contracts/openEditionMinter/">Open Edition Minter Contract</Link>
|
||||
</li>
|
||||
</Conditional>
|
||||
<li
|
||||
className={clsx(
|
||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
||||
|
@ -15,6 +15,7 @@ import { JsonPreview } from 'components/JsonPreview'
|
||||
import { TransactionHash } from 'components/TransactionHash'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { BaseMinterInstance } from 'contracts/baseMinter'
|
||||
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter'
|
||||
import type { SG721Instance } from 'contracts/sg721'
|
||||
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
||||
import type { FormEvent } from 'react'
|
||||
@ -35,6 +36,7 @@ interface CollectionActionsProps {
|
||||
sg721Messages: SG721Instance | undefined
|
||||
vendingMinterMessages: VendingMinterInstance | undefined
|
||||
baseMinterMessages: BaseMinterInstance | undefined
|
||||
openEditionMinterMessages: OpenEditionMinterInstance | undefined
|
||||
minterType: MinterType
|
||||
sg721Type: Sg721Type
|
||||
}
|
||||
@ -47,6 +49,7 @@ export const CollectionActions = ({
|
||||
minterContractAddress,
|
||||
vendingMinterMessages,
|
||||
baseMinterMessages,
|
||||
openEditionMinterMessages,
|
||||
minterType,
|
||||
sg721Type,
|
||||
}: CollectionActionsProps) => {
|
||||
@ -54,6 +57,7 @@ export const CollectionActions = ({
|
||||
const [lastTx, setLastTx] = useState('')
|
||||
|
||||
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
||||
const [endTimestamp, setEndTimestamp] = useState<Date | undefined>(undefined)
|
||||
const [airdropAllocationArray, setAirdropAllocationArray] = useState<AirdropAllocation[]>([])
|
||||
const [airdropArray, setAirdropArray] = useState<string[]>([])
|
||||
const [collectionInfo, setCollectionInfo] = useState<CollectionInfo>()
|
||||
@ -168,6 +172,7 @@ export const CollectionActions = ({
|
||||
const showTokenUriField = isEitherType(type, ['mint_token_uri', 'update_token_metadata'])
|
||||
const showWhitelistField = type === 'set_whitelist'
|
||||
const showDateField = isEitherType(type, ['update_start_time', 'update_start_trading_time'])
|
||||
const showEndDateField = type === 'update_end_time'
|
||||
const showLimitField = type === 'update_per_address_limit'
|
||||
const showTokenIdField = isEitherType(type, ['transfer', 'mint_for', 'burn', 'update_token_metadata'])
|
||||
const showNumberOfTokensField = type === 'batch_mint'
|
||||
@ -197,6 +202,7 @@ export const CollectionActions = ({
|
||||
const payload: DispatchExecuteArgs = {
|
||||
whitelist: whitelistState.value,
|
||||
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
|
||||
endTime: endTimestamp ? (endTimestamp.getTime() * 1_000_000).toString() : '',
|
||||
limit: limitState.value,
|
||||
minterContract: minterContractAddress,
|
||||
sg721Contract: sg721ContractAddress,
|
||||
@ -208,6 +214,7 @@ export const CollectionActions = ({
|
||||
batchNumber: batchNumberState.value,
|
||||
vendingMinterMessages,
|
||||
baseMinterMessages,
|
||||
openEditionMinterMessages,
|
||||
sg721Messages,
|
||||
recipient: resolvedRecipientAddress,
|
||||
recipients: airdropArray,
|
||||
@ -493,6 +500,11 @@ export const CollectionActions = ({
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
|
||||
</FormControl>
|
||||
</Conditional>
|
||||
<Conditional test={showEndDateField}>
|
||||
<FormControl className="mt-2" htmlId="end-date" title="End Time">
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setEndTimestamp(date)} value={endTimestamp} />
|
||||
</FormControl>
|
||||
</Conditional>
|
||||
</div>
|
||||
<div className="-mt-6">
|
||||
<div className="relative mb-2">
|
||||
|
@ -6,9 +6,9 @@ import { Fragment, useEffect, useState } from 'react'
|
||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||
|
||||
import type { ActionListItem } from './actions'
|
||||
import { BASE_ACTION_LIST, SG721_UPDATABLE_ACTION_LIST, VENDING_ACTION_LIST } from './actions'
|
||||
import { BASE_ACTION_LIST, OPEN_EDITION_ACTION_LIST, SG721_UPDATABLE_ACTION_LIST, VENDING_ACTION_LIST } from './actions'
|
||||
|
||||
export type MinterType = 'base' | 'vending'
|
||||
export type MinterType = 'base' | 'vending' | 'openEdition'
|
||||
export type Sg721Type = 'updatable' | 'base'
|
||||
|
||||
export interface ActionsComboboxProps {
|
||||
@ -29,6 +29,9 @@ export const ActionsCombobox = ({ value, onChange, minterType, sg721Type }: Acti
|
||||
} else if (minterType === 'vending') {
|
||||
if (sg721Type === 'updatable') SET_ACTION_LIST(VENDING_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST))
|
||||
else SET_ACTION_LIST(VENDING_ACTION_LIST)
|
||||
} else if (minterType === 'openEdition') {
|
||||
if (sg721Type === 'updatable') SET_ACTION_LIST(OPEN_EDITION_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST))
|
||||
else SET_ACTION_LIST(OPEN_EDITION_ACTION_LIST)
|
||||
} else SET_ACTION_LIST(VENDING_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST))
|
||||
}, [minterType, sg721Type])
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
|
||||
import { useBaseMinterContract } from 'contracts/baseMinter'
|
||||
import { useOpenEditionMinterContract } from 'contracts/openEditionMinter'
|
||||
import type { CollectionInfo, SG721Instance } from 'contracts/sg721'
|
||||
import { useSG721Contract } from 'contracts/sg721'
|
||||
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
||||
@ -8,6 +8,7 @@ import { useVendingMinterContract } from 'contracts/vendingMinter'
|
||||
import type { AirdropAllocation } from 'utils/isValidAccountsFile'
|
||||
|
||||
import type { BaseMinterInstance } from '../../../contracts/baseMinter/contract'
|
||||
import type { OpenEditionMinterInstance } from '../../../contracts/openEditionMinter/contract'
|
||||
|
||||
export type ActionType = typeof ACTION_TYPES[number]
|
||||
|
||||
@ -21,6 +22,7 @@ export const ACTION_TYPES = [
|
||||
'batch_mint',
|
||||
'set_whitelist',
|
||||
'update_start_time',
|
||||
'update_end_time',
|
||||
'update_start_trading_time',
|
||||
'update_per_address_limit',
|
||||
'update_collection_info',
|
||||
@ -197,6 +199,79 @@ export const VENDING_ACTION_LIST: ActionListItem[] = [
|
||||
},
|
||||
]
|
||||
|
||||
export const OPEN_EDITION_ACTION_LIST: ActionListItem[] = [
|
||||
{
|
||||
id: 'update_mint_price',
|
||||
name: 'Update Mint Price',
|
||||
description: `Update mint price`,
|
||||
},
|
||||
{
|
||||
id: 'mint_to',
|
||||
name: 'Mint To',
|
||||
description: `Mint a token to a user`,
|
||||
},
|
||||
{
|
||||
id: 'batch_mint',
|
||||
name: 'Batch Mint To',
|
||||
description: `Mint multiple tokens to a user`,
|
||||
},
|
||||
{
|
||||
id: 'update_start_time',
|
||||
name: 'Update Minting Start Time',
|
||||
description: `Update the start time for minting`,
|
||||
},
|
||||
{
|
||||
id: 'update_end_time',
|
||||
name: 'Update Minting End Time',
|
||||
description: `Update the end time for minting`,
|
||||
},
|
||||
{
|
||||
id: 'update_start_trading_time',
|
||||
name: 'Update Trading Start Time',
|
||||
description: `Update start time for trading`,
|
||||
},
|
||||
{
|
||||
id: 'update_per_address_limit',
|
||||
name: 'Update Tokens Per Address Limit',
|
||||
description: `Update token per address limit`,
|
||||
},
|
||||
{
|
||||
id: 'update_collection_info',
|
||||
name: 'Update Collection Info',
|
||||
description: `Update Collection Info`,
|
||||
},
|
||||
{
|
||||
id: 'freeze_collection_info',
|
||||
name: 'Freeze Collection Info',
|
||||
description: `Freeze collection info to prevent further updates`,
|
||||
},
|
||||
{
|
||||
id: 'transfer',
|
||||
name: 'Transfer Tokens',
|
||||
description: `Transfer tokens from one address to another`,
|
||||
},
|
||||
{
|
||||
id: 'batch_transfer',
|
||||
name: 'Batch Transfer Tokens',
|
||||
description: `Transfer a list of tokens to a recipient`,
|
||||
},
|
||||
{
|
||||
id: 'burn',
|
||||
name: 'Burn Token',
|
||||
description: `Burn a specified token from the collection`,
|
||||
},
|
||||
{
|
||||
id: 'batch_burn',
|
||||
name: 'Batch Burn Tokens',
|
||||
description: `Burn a list of tokens from the collection`,
|
||||
},
|
||||
{
|
||||
id: 'airdrop',
|
||||
name: 'Airdrop Tokens',
|
||||
description: 'Airdrop tokens to given addresses',
|
||||
},
|
||||
]
|
||||
|
||||
export const SG721_UPDATABLE_ACTION_LIST: ActionListItem[] = [
|
||||
{
|
||||
id: 'update_token_metadata',
|
||||
@ -231,6 +306,7 @@ export interface DispatchExecuteArgs {
|
||||
sg721Contract: string
|
||||
vendingMinterMessages?: VendingMinterInstance
|
||||
baseMinterMessages?: BaseMinterInstance
|
||||
openEditionMinterMessages?: OpenEditionMinterInstance
|
||||
sg721Messages?: SG721Instance
|
||||
txSigner: string
|
||||
type: string | undefined
|
||||
@ -241,6 +317,7 @@ export interface DispatchExecuteArgs {
|
||||
batchNumber: number
|
||||
whitelist: string
|
||||
startTime: string | undefined
|
||||
endTime: string | undefined
|
||||
limit: number
|
||||
tokenIds: string
|
||||
recipients: string[]
|
||||
@ -250,8 +327,8 @@ export interface DispatchExecuteArgs {
|
||||
}
|
||||
|
||||
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
const { vendingMinterMessages, baseMinterMessages, sg721Messages, txSigner } = args
|
||||
if (!vendingMinterMessages || !baseMinterMessages || !sg721Messages) {
|
||||
const { vendingMinterMessages, baseMinterMessages, openEditionMinterMessages, sg721Messages, txSigner } = args
|
||||
if (!vendingMinterMessages || !baseMinterMessages || !openEditionMinterMessages || !sg721Messages) {
|
||||
throw new Error('Cannot execute actions')
|
||||
}
|
||||
switch (args.type) {
|
||||
@ -282,6 +359,9 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
case 'update_start_time': {
|
||||
return vendingMinterMessages.updateStartTime(txSigner, args.startTime as string)
|
||||
}
|
||||
case 'update_end_time': {
|
||||
return openEditionMinterMessages.updateEndTime(txSigner, args.endTime as string)
|
||||
}
|
||||
case 'update_start_trading_time': {
|
||||
return vendingMinterMessages.updateStartTradingTime(txSigner, args.startTime)
|
||||
}
|
||||
@ -346,6 +426,8 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
const { messages: sg721Messages } = useSG721Contract()
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages: baseMinterMessages } = useBaseMinterContract()
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages: openEditionMinterMessages } = useOpenEditionMinterContract()
|
||||
const { minterContract, sg721Contract } = args
|
||||
switch (args.type) {
|
||||
case 'mint_token_uri': {
|
||||
@ -375,6 +457,9 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
case 'update_start_time': {
|
||||
return vendingMinterMessages(minterContract)?.updateStartTime(args.startTime as string)
|
||||
}
|
||||
case 'update_end_time': {
|
||||
return openEditionMinterMessages(minterContract)?.updateEndTime(args.endTime as string)
|
||||
}
|
||||
case 'update_start_trading_time': {
|
||||
return vendingMinterMessages(minterContract)?.updateStartTradingTime(args.startTime as string)
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||
|
||||
import type { MinterType } from '../actions/Combobox'
|
||||
import type { QueryListItem } from './query'
|
||||
import { BASE_QUERY_LIST, VENDING_QUERY_LIST } from './query'
|
||||
import { BASE_QUERY_LIST, OPEN_EDITION_QUERY_LIST, VENDING_QUERY_LIST } from './query'
|
||||
|
||||
export interface QueryComboboxProps {
|
||||
value: QueryListItem | null
|
||||
@ -22,6 +22,8 @@ export const QueryCombobox = ({ value, onChange, minterType }: QueryComboboxProp
|
||||
useEffect(() => {
|
||||
if (minterType === 'base') {
|
||||
SET_QUERY_LIST(BASE_QUERY_LIST)
|
||||
} else if (minterType === 'openEdition') {
|
||||
SET_QUERY_LIST(OPEN_EDITION_QUERY_LIST)
|
||||
} else {
|
||||
SET_QUERY_LIST(VENDING_QUERY_LIST)
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import { AddressInput, TextInput } from 'components/forms/FormInput'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { JsonPreview } from 'components/JsonPreview'
|
||||
import type { BaseMinterInstance } from 'contracts/baseMinter'
|
||||
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter'
|
||||
import type { SG721Instance } from 'contracts/sg721'
|
||||
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
||||
import { toast } from 'react-hot-toast'
|
||||
@ -21,6 +22,7 @@ interface CollectionQueriesProps {
|
||||
sg721Messages: SG721Instance | undefined
|
||||
vendingMinterMessages: VendingMinterInstance | undefined
|
||||
baseMinterMessages: BaseMinterInstance | undefined
|
||||
openEditionMinterMessages: OpenEditionMinterInstance | undefined
|
||||
minterType: MinterType
|
||||
}
|
||||
export const CollectionQueries = ({
|
||||
@ -28,6 +30,7 @@ export const CollectionQueries = ({
|
||||
sg721Messages,
|
||||
minterContractAddress,
|
||||
vendingMinterMessages,
|
||||
openEditionMinterMessages,
|
||||
baseMinterMessages,
|
||||
minterType,
|
||||
}: CollectionQueriesProps) => {
|
||||
@ -57,9 +60,25 @@ export const CollectionQueries = ({
|
||||
const showAddressField = type === 'tokens_minted_to_user' || type === 'tokens'
|
||||
|
||||
const { data: response } = useQuery(
|
||||
[sg721Messages, baseMinterMessages, vendingMinterMessages, type, tokenId, address] as const,
|
||||
[
|
||||
sg721Messages,
|
||||
baseMinterMessages,
|
||||
vendingMinterMessages,
|
||||
openEditionMinterMessages,
|
||||
type,
|
||||
tokenId,
|
||||
address,
|
||||
] as const,
|
||||
async ({ queryKey }) => {
|
||||
const [_sg721Messages, _baseMinterMessages_, _vendingMinterMessages, _type, _tokenId, _address] = queryKey
|
||||
const [
|
||||
_sg721Messages,
|
||||
_baseMinterMessages_,
|
||||
_vendingMinterMessages,
|
||||
_openEditionMinterMessages,
|
||||
_type,
|
||||
_tokenId,
|
||||
_address,
|
||||
] = queryKey
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const res = await resolveAddress(_address, wallet).then(async (resolvedAddress) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
@ -67,6 +86,7 @@ export const CollectionQueries = ({
|
||||
tokenId: _tokenId,
|
||||
vendingMinterMessages: _vendingMinterMessages,
|
||||
baseMinterMessages: _baseMinterMessages_,
|
||||
openEditionMinterMessages: _openEditionMinterMessages,
|
||||
sg721Messages: _sg721Messages,
|
||||
address: resolvedAddress,
|
||||
type: _type,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import type { BaseMinterInstance } from 'contracts/baseMinter'
|
||||
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter/contract'
|
||||
import type { SG721Instance } from 'contracts/sg721'
|
||||
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
||||
|
||||
@ -9,6 +10,7 @@ export const QUERY_TYPES = [
|
||||
'mint_price',
|
||||
'num_tokens',
|
||||
'tokens_minted_to_user',
|
||||
'total_mint_count',
|
||||
'tokens',
|
||||
// 'token_owners',
|
||||
'token_info',
|
||||
@ -91,6 +93,48 @@ export const BASE_QUERY_LIST: QueryListItem[] = [
|
||||
description: `Query Minter Status`,
|
||||
},
|
||||
]
|
||||
export const OPEN_EDITION_QUERY_LIST: QueryListItem[] = [
|
||||
{
|
||||
id: 'collection_info',
|
||||
name: 'Collection Info',
|
||||
description: `Get information about the collection.`,
|
||||
},
|
||||
{
|
||||
id: 'mint_price',
|
||||
name: 'Mint Price',
|
||||
description: `Get the price of minting a token.`,
|
||||
},
|
||||
{
|
||||
id: 'tokens_minted_to_user',
|
||||
name: 'Tokens Minted to User',
|
||||
description: `Get the number of tokens minted in the collection to a user.`,
|
||||
},
|
||||
{
|
||||
id: 'total_mint_count',
|
||||
name: 'Total Mint Count',
|
||||
description: `Get the total number of tokens minted for the collection.`,
|
||||
},
|
||||
// {
|
||||
// id: 'token_owners',
|
||||
// name: 'Token Owners',
|
||||
// description: `Get the list of users who own tokens in the collection.`,
|
||||
// },
|
||||
{
|
||||
id: 'token_info',
|
||||
name: 'Token Info',
|
||||
description: `Get information about a token in the collection.`,
|
||||
},
|
||||
{
|
||||
id: 'config',
|
||||
name: 'Minter Config',
|
||||
description: `Query Minter Config`,
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
name: 'Minter Status',
|
||||
description: `Query Minter Status`,
|
||||
},
|
||||
]
|
||||
|
||||
export interface DispatchExecuteProps {
|
||||
type: QueryType
|
||||
@ -102,6 +146,7 @@ type Select<T extends QueryType> = T
|
||||
export type DispatchQueryArgs = {
|
||||
baseMinterMessages?: BaseMinterInstance
|
||||
vendingMinterMessages?: VendingMinterInstance
|
||||
openEditionMinterMessages?: OpenEditionMinterInstance
|
||||
sg721Messages?: SG721Instance
|
||||
} & (
|
||||
| { type: undefined }
|
||||
@ -109,6 +154,7 @@ export type DispatchQueryArgs = {
|
||||
| { type: Select<'mint_price'> }
|
||||
| { type: Select<'num_tokens'> }
|
||||
| { type: Select<'tokens_minted_to_user'>; address: string }
|
||||
| { type: Select<'total_mint_count'> }
|
||||
| { type: Select<'tokens'>; address: string }
|
||||
// | { type: Select<'token_owners'> }
|
||||
| { type: Select<'token_info'>; tokenId: string }
|
||||
@ -117,8 +163,8 @@ export type DispatchQueryArgs = {
|
||||
)
|
||||
|
||||
export const dispatchQuery = async (args: DispatchQueryArgs) => {
|
||||
const { baseMinterMessages, vendingMinterMessages, sg721Messages } = args
|
||||
if (!baseMinterMessages || !vendingMinterMessages || !sg721Messages) {
|
||||
const { baseMinterMessages, vendingMinterMessages, openEditionMinterMessages, sg721Messages } = args
|
||||
if (!baseMinterMessages || !vendingMinterMessages || !openEditionMinterMessages || !sg721Messages) {
|
||||
throw new Error('Cannot execute actions')
|
||||
}
|
||||
switch (args.type) {
|
||||
@ -134,6 +180,9 @@ export const dispatchQuery = async (args: DispatchQueryArgs) => {
|
||||
case 'tokens_minted_to_user': {
|
||||
return vendingMinterMessages.getMintCount(args.address)
|
||||
}
|
||||
case 'total_mint_count': {
|
||||
return openEditionMinterMessages.getTotalMintCount()
|
||||
}
|
||||
case 'tokens': {
|
||||
return sg721Messages.tokens(args.address)
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
import type { ExecuteListItem } from 'contracts/openEditionMinter/messages/execute'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const useExecuteComboboxState = () => {
|
||||
const [value, setValue] = useState<ExecuteListItem | null>(null)
|
||||
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
|
||||
}
|
92
components/contracts/openEditionMinter/ExecuteCombobox.tsx
Normal file
92
components/contracts/openEditionMinter/ExecuteCombobox.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import { Combobox, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import type { ExecuteListItem } from 'contracts/openEditionMinter/messages/execute'
|
||||
import { EXECUTE_LIST } from 'contracts/openEditionMinter/messages/execute'
|
||||
import { matchSorter } from 'match-sorter'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||
|
||||
export interface ExecuteComboboxProps {
|
||||
value: ExecuteListItem | null
|
||||
onChange: (item: ExecuteListItem) => void
|
||||
}
|
||||
|
||||
export const ExecuteCombobox = ({ value, onChange }: ExecuteComboboxProps) => {
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const filtered =
|
||||
search === '' ? EXECUTE_LIST : matchSorter(EXECUTE_LIST, search, { keys: ['id', 'name', 'description'] })
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
as={FormControl}
|
||||
htmlId="message-type"
|
||||
labelAs={Combobox.Label}
|
||||
onChange={onChange}
|
||||
subtitle="Contract execute message type"
|
||||
title="Message Type"
|
||||
value={value}
|
||||
>
|
||||
<div className="relative">
|
||||
<Combobox.Input
|
||||
className={clsx(
|
||||
'w-full bg-white/10 rounded border-2 border-white/20 form-input',
|
||||
'placeholder:text-white/50',
|
||||
'focus:ring focus:ring-plumbus-20',
|
||||
)}
|
||||
displayValue={(val?: ExecuteListItem) => val?.name ?? ''}
|
||||
id="message-type"
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder="Select message type"
|
||||
/>
|
||||
|
||||
<Combobox.Button
|
||||
className={clsx(
|
||||
'flex absolute inset-y-0 right-0 items-center p-4',
|
||||
'opacity-50 hover:opacity-100 active:opacity-100',
|
||||
)}
|
||||
>
|
||||
{({ open }) => <FaChevronDown aria-hidden="true" className={clsx('w-4 h-4', { 'rotate-180': open })} />}
|
||||
</Combobox.Button>
|
||||
|
||||
<Transition afterLeave={() => setSearch('')} as={Fragment}>
|
||||
<Combobox.Options
|
||||
className={clsx(
|
||||
'overflow-auto absolute z-10 mt-2 w-full max-h-[30vh]',
|
||||
'bg-stone-800/80 rounded shadow-lg backdrop-blur-sm',
|
||||
'divide-y divide-stone-500/50',
|
||||
)}
|
||||
>
|
||||
{filtered.length < 1 && (
|
||||
<span className="flex flex-col justify-center items-center p-4 text-sm text-center text-white/50">
|
||||
Message type not found.
|
||||
</span>
|
||||
)}
|
||||
{filtered.map((entry) => (
|
||||
<Combobox.Option
|
||||
key={entry.id}
|
||||
className={({ active }) =>
|
||||
clsx('flex relative flex-col py-2 px-4 space-y-1 cursor-pointer', { 'bg-stargaze-80': active })
|
||||
}
|
||||
value={entry}
|
||||
>
|
||||
<span className="font-bold">{entry.name}</span>
|
||||
<span className="max-w-md text-sm">{entry.description}</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
{value && (
|
||||
<div className="flex space-x-2 text-white/50">
|
||||
<div className="mt-1">
|
||||
<FaInfoCircle className="w-3 h-3" />
|
||||
</div>
|
||||
<span className="text-sm">{value.description}</span>
|
||||
</div>
|
||||
)}
|
||||
</Combobox>
|
||||
)
|
||||
}
|
293
components/openEdition/CollectionDetails.tsx
Normal file
293
components/openEdition/CollectionDetails.tsx
Normal file
@ -0,0 +1,293 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { InputDateTime } from 'components/InputDateTime'
|
||||
import { Tooltip } from 'components/Tooltip'
|
||||
import { addLogItem } from 'contexts/log'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS, SG721_UPDATABLE_CODE_ID } from 'utils/constants'
|
||||
import { uid } from 'utils/random'
|
||||
|
||||
import { TextInput } from '../forms/FormInput'
|
||||
import type { UploadMethod } from './OffChainMetadataUploadDetails'
|
||||
import type { MetadataStorageMethod } from './OpenEditionMinterCreator'
|
||||
|
||||
interface CollectionDetailsProps {
|
||||
onChange: (data: CollectionDetailsDataProps) => void
|
||||
uploadMethod: UploadMethod
|
||||
coverImageUrl: string
|
||||
metadataStorageMethod: MetadataStorageMethod
|
||||
}
|
||||
|
||||
export interface CollectionDetailsDataProps {
|
||||
name: string
|
||||
description: string
|
||||
symbol: string
|
||||
imageFile: File[]
|
||||
externalLink?: string
|
||||
startTradingTime?: string
|
||||
explicit: boolean
|
||||
updatable: boolean
|
||||
}
|
||||
|
||||
export const CollectionDetails = ({
|
||||
onChange,
|
||||
uploadMethod,
|
||||
metadataStorageMethod,
|
||||
coverImageUrl,
|
||||
}: CollectionDetailsProps) => {
|
||||
const [coverImage, setCoverImage] = useState<File | null>(null)
|
||||
const [timestamp, setTimestamp] = useState<Date | undefined>()
|
||||
const [explicit, setExplicit] = useState<boolean>(false)
|
||||
const [updatable, setUpdatable] = useState<boolean>(false)
|
||||
|
||||
const initialRender = useRef(true)
|
||||
|
||||
const coverImageInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const nameState = useInputState({
|
||||
id: 'name',
|
||||
name: 'name',
|
||||
title: 'Name',
|
||||
placeholder: 'My Awesome Collection',
|
||||
})
|
||||
|
||||
const descriptionState = useInputState({
|
||||
id: 'description',
|
||||
name: 'description',
|
||||
title: 'Description',
|
||||
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 (optional)',
|
||||
placeholder: 'https://my-collection...',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const data: CollectionDetailsDataProps = {
|
||||
name: nameState.value,
|
||||
description: descriptionState.value,
|
||||
symbol: symbolState.value,
|
||||
imageFile: coverImage ? [coverImage] : [],
|
||||
externalLink: externalLinkState.value || undefined,
|
||||
startTradingTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
|
||||
explicit,
|
||||
updatable,
|
||||
}
|
||||
onChange(data)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
nameState.value,
|
||||
descriptionState.value,
|
||||
symbolState.value,
|
||||
externalLinkState.value,
|
||||
coverImage,
|
||||
timestamp,
|
||||
explicit,
|
||||
updatable,
|
||||
])
|
||||
|
||||
const selectCoverImage = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.files === null) return toast.error('Error selecting cover image')
|
||||
if (event.target.files.length === 0) {
|
||||
setCoverImage(null)
|
||||
return toast.error('No files selected.')
|
||||
}
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
||||
if (!event.target.files) return toast.error('No files selected.')
|
||||
const imageFile = new File([e.target.result], event.target.files[0].name, { type: 'image/jpg' })
|
||||
setCoverImage(imageFile)
|
||||
}
|
||||
reader.readAsArrayBuffer(event.target.files[0])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setCoverImage(null)
|
||||
// empty the element so that the same file can be selected again
|
||||
if (coverImageInputRef.current) coverImageInputRef.current.value = ''
|
||||
}, [metadataStorageMethod])
|
||||
|
||||
useEffect(() => {
|
||||
if (initialRender.current) {
|
||||
initialRender.current = false
|
||||
} else if (updatable) {
|
||||
toast.success('Token metadata will be updatable upon collection creation.', {
|
||||
style: { maxWidth: 'none' },
|
||||
icon: '✅📝',
|
||||
})
|
||||
} else {
|
||||
toast.error('Token metadata will not be updatable upon collection creation.', {
|
||||
style: { maxWidth: 'none' },
|
||||
icon: '⛔🔏',
|
||||
})
|
||||
}
|
||||
}, [updatable])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormGroup subtitle="Information about your collection" title="Collection Details">
|
||||
<div className={clsx('')}>
|
||||
<div className="">
|
||||
<TextInput {...nameState} isRequired />
|
||||
<TextInput className="mt-2" {...descriptionState} isRequired />
|
||||
<TextInput className="mt-2" {...symbolState} isRequired />
|
||||
</div>
|
||||
<div className={clsx('')}>
|
||||
<TextInput className={clsx('mt-2')} {...externalLinkState} />
|
||||
{/* Currently trading starts immediately for 1/1 Collections */}
|
||||
|
||||
<FormControl
|
||||
className={clsx('mt-2')}
|
||||
htmlId="timestamp"
|
||||
subtitle="Trading start time offset will be set as 2 weeks by default."
|
||||
title="Trading Start Time (optional)"
|
||||
>
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormControl className={clsx('')} isRequired={uploadMethod === 'new'} title="Cover Image">
|
||||
{uploadMethod === 'new' && (
|
||||
<input
|
||||
accept="image/*"
|
||||
className={clsx(
|
||||
'w-full',
|
||||
'p-[13px] rounded border-2 border-white/20 border-dashed cursor-pointer h-18',
|
||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
||||
'before:hover:bg-white/5 before:transition',
|
||||
)}
|
||||
id="cover-image"
|
||||
onChange={selectCoverImage}
|
||||
ref={coverImageInputRef}
|
||||
type="file"
|
||||
/>
|
||||
)}
|
||||
|
||||
{coverImage !== null && uploadMethod === 'new' && (
|
||||
<div className="max-w-[200px] max-h-[200px] rounded border-2">
|
||||
<img alt="no-preview-available" src={URL.createObjectURL(coverImage)} />
|
||||
</div>
|
||||
)}
|
||||
{uploadMethod === 'existing' && coverImageUrl?.includes('ipfs://') && (
|
||||
<div className="max-w-[200px] max-h-[200px] rounded border-2">
|
||||
<img
|
||||
alt="no-preview-available"
|
||||
src={`https://ipfs-gw.stargaze-apis.com/ipfs/${coverImageUrl.substring(
|
||||
coverImageUrl.lastIndexOf('ipfs://') + 7,
|
||||
)}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{uploadMethod === 'existing' && coverImageUrl && !coverImageUrl?.includes('ipfs://') && (
|
||||
<div className="max-w-[200px] max-h-[200px] rounded border-2">
|
||||
<img alt="no-preview-available" src={coverImageUrl} />
|
||||
</div>
|
||||
)}
|
||||
{uploadMethod === 'existing' && !coverImageUrl && (
|
||||
<span className="italic font-light ">Waiting for cover image URL to be specified.</span>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<div className={clsx('flex flex-col space-y-2')}>
|
||||
<div>
|
||||
<div className="flex mt-4">
|
||||
<span className="mt-1 ml-[2px] text-sm first-letter:capitalize">
|
||||
Does the collection contain explicit content?
|
||||
</span>
|
||||
<div className="ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={explicit}
|
||||
className="peer sr-only"
|
||||
id="explicitRadio1"
|
||||
name="explicitRadioOptions1"
|
||||
onClick={() => {
|
||||
setExplicit(true)
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="explicitRadio1"
|
||||
>
|
||||
YES
|
||||
</label>
|
||||
</div>
|
||||
<div className="ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={!explicit}
|
||||
className="peer sr-only"
|
||||
id="explicitRadio2"
|
||||
name="explicitRadioOptions2"
|
||||
onClick={() => {
|
||||
setExplicit(false)
|
||||
}}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-sm text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="explicitRadio2"
|
||||
>
|
||||
NO
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Conditional test={SG721_UPDATABLE_CODE_ID > 0 && OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS !== undefined}>
|
||||
<Tooltip
|
||||
backgroundColor="bg-blue-500"
|
||||
label={
|
||||
<div className="grid grid-flow-row">
|
||||
<span>
|
||||
ℹ️ When enabled, the metadata for tokens can be updated after the collection is created until the
|
||||
collection is frozen by the creator.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
placement="bottom"
|
||||
>
|
||||
<div className={clsx('flex flex-col space-y-2 w-3/4 form-control')}>
|
||||
<label className="justify-start cursor-pointer label">
|
||||
<span className="mr-4 font-bold">Updatable Token Metadata</span>
|
||||
<input
|
||||
checked={updatable}
|
||||
className={`toggle ${updatable ? `bg-stargaze` : `bg-gray-600`}`}
|
||||
onClick={() => setUpdatable(!updatable)}
|
||||
type="checkbox"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Conditional>
|
||||
</FormGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
349
components/openEdition/ImageUploadDetails.tsx
Normal file
349
components/openEdition/ImageUploadDetails.tsx
Normal file
@ -0,0 +1,349 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable jsx-a11y/media-has-caption */
|
||||
|
||||
/* eslint-disable no-misleading-character-class */
|
||||
/* eslint-disable no-control-regex */
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { Anchor } from 'components/Anchor'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { TextInput } from 'components/forms/FormInput'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { SingleAssetPreview } from 'components/SingleAssetPreview'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import type { UploadServiceType } from 'services/upload'
|
||||
import { getAssetType } from 'utils/getAssetType'
|
||||
|
||||
export type UploadMethod = 'new' | 'existing'
|
||||
|
||||
interface ImageUploadDetailsProps {
|
||||
onChange: (value: ImageUploadDetailsDataProps) => void
|
||||
}
|
||||
|
||||
export interface ImageUploadDetailsDataProps {
|
||||
assetFile: File | undefined
|
||||
uploadService: UploadServiceType
|
||||
nftStorageApiKey?: string
|
||||
pinataApiKey?: string
|
||||
pinataSecretKey?: string
|
||||
uploadMethod: UploadMethod
|
||||
imageUrl?: string
|
||||
coverImageUrl?: string
|
||||
}
|
||||
|
||||
export const ImageUploadDetails = ({ onChange }: ImageUploadDetailsProps) => {
|
||||
const [assetFile, setAssetFile] = useState<File>()
|
||||
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
|
||||
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
|
||||
|
||||
const assetFileRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const nftStorageApiKeyState = useInputState({
|
||||
id: 'nft-storage-api-key',
|
||||
name: 'nftStorageApiKey',
|
||||
title: 'NFT.Storage API Key',
|
||||
placeholder: 'Enter NFT.Storage API Key',
|
||||
defaultValue: '',
|
||||
})
|
||||
const pinataApiKeyState = useInputState({
|
||||
id: 'pinata-api-key',
|
||||
name: 'pinataApiKey',
|
||||
title: 'Pinata API Key',
|
||||
placeholder: 'Enter Pinata API Key',
|
||||
defaultValue: '',
|
||||
})
|
||||
const pinataSecretKeyState = useInputState({
|
||||
id: 'pinata-secret-key',
|
||||
name: 'pinataSecretKey',
|
||||
title: 'Pinata Secret Key',
|
||||
placeholder: 'Enter Pinata Secret Key',
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const imageUrlState = useInputState({
|
||||
id: 'imageUrl',
|
||||
name: 'imageUrl',
|
||||
title: 'Asset URL',
|
||||
placeholder: 'ipfs://',
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const coverImageUrlState = useInputState({
|
||||
id: 'coverImageUrl',
|
||||
name: 'coverImageUrl',
|
||||
title: 'Cover Image URL',
|
||||
placeholder: 'ipfs://',
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const selectAsset = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setAssetFile(undefined)
|
||||
if (event.target.files === null) return
|
||||
|
||||
let selectedFile: File
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
if (!event.target.files) return toast.error('No file selected.')
|
||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
||||
selectedFile = new File([e.target.result], event.target.files[0].name, { type: 'image/jpg' })
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
|
||||
else return toast.error('No file selected.')
|
||||
reader.onloadend = () => {
|
||||
if (!event.target.files) return toast.error('No file selected.')
|
||||
setAssetFile(selectedFile)
|
||||
}
|
||||
}
|
||||
|
||||
const regex =
|
||||
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const data: ImageUploadDetailsDataProps = {
|
||||
assetFile,
|
||||
uploadService,
|
||||
nftStorageApiKey: nftStorageApiKeyState.value,
|
||||
pinataApiKey: pinataApiKeyState.value,
|
||||
pinataSecretKey: pinataSecretKeyState.value,
|
||||
uploadMethod,
|
||||
imageUrl: imageUrlState.value
|
||||
.replace('IPFS://', 'ipfs://')
|
||||
.replace(/,/g, '')
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '')
|
||||
.replace(/ /g, '')
|
||||
.replace(regex, ''),
|
||||
coverImageUrl: coverImageUrlState.value.trim(),
|
||||
}
|
||||
onChange(data)
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
}
|
||||
}, [
|
||||
assetFile,
|
||||
uploadService,
|
||||
nftStorageApiKeyState.value,
|
||||
pinataApiKeyState.value,
|
||||
pinataSecretKeyState.value,
|
||||
uploadMethod,
|
||||
imageUrlState.value,
|
||||
coverImageUrlState.value,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (assetFileRef.current) assetFileRef.current.value = ''
|
||||
setAssetFile(undefined)
|
||||
imageUrlState.onChange('')
|
||||
}, [uploadMethod])
|
||||
|
||||
const previewUrl = imageUrlState.value.toLowerCase().trim().startsWith('ipfs://')
|
||||
? `https://ipfs-gw.stargaze-apis.com/ipfs/${imageUrlState.value.substring(7)}`
|
||||
: imageUrlState.value
|
||||
|
||||
const audioPreview = useMemo(
|
||||
() => (
|
||||
<audio
|
||||
controls
|
||||
id="audio"
|
||||
onMouseEnter={(e) => e.currentTarget.play()}
|
||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||
src={imageUrlState.value ? previewUrl : ''}
|
||||
/>
|
||||
),
|
||||
[imageUrlState.value],
|
||||
)
|
||||
|
||||
const videoPreview = useMemo(
|
||||
() => (
|
||||
<video
|
||||
controls
|
||||
id="video"
|
||||
onMouseEnter={(e) => e.currentTarget.play()}
|
||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||
src={imageUrlState.value ? previewUrl : ''}
|
||||
/>
|
||||
),
|
||||
[imageUrlState.value],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
|
||||
<div className="flex justify-center">
|
||||
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadMethod === 'new'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio2"
|
||||
name="inlineRadioOptions2"
|
||||
onClick={() => {
|
||||
setUploadMethod('new')
|
||||
}}
|
||||
type="radio"
|
||||
value="New"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio2"
|
||||
>
|
||||
Upload New Asset
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadMethod === 'existing'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio1"
|
||||
name="inlineRadioOptions1"
|
||||
onClick={() => {
|
||||
setUploadMethod('existing')
|
||||
}}
|
||||
type="radio"
|
||||
value="Existing"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio1"
|
||||
>
|
||||
Use an existing Asset URL
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 py-5 pb-4">
|
||||
<Conditional test={uploadMethod === 'existing'}>
|
||||
<div className="ml-3 flex-column">
|
||||
<p className="mb-5 ml-5">
|
||||
Though the Open Edition contracts allow for off-chain asset storage, it is recommended to use a
|
||||
decentralized storage solution, such as IPFS. <br /> You may head over to{' '}
|
||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://nft.storage">
|
||||
NFT.Storage
|
||||
</Anchor>{' '}
|
||||
or{' '}
|
||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://www.pinata.cloud/">
|
||||
Pinata
|
||||
</Anchor>{' '}
|
||||
and upload your asset manually to get an asset URL for your NFT.
|
||||
</p>
|
||||
<div className="flex flex-row w-full">
|
||||
<div className="flex flex-col w-3/5">
|
||||
<TextInput {...imageUrlState} className="mt-2 ml-6" />
|
||||
<TextInput {...coverImageUrlState} className="mt-4 ml-6" />
|
||||
</div>
|
||||
<Conditional test={imageUrlState.value !== ''}>
|
||||
<div className="flex mt-2 ml-8 w-1/3 border-2 border-dashed">
|
||||
{getAssetType(imageUrlState.value) === 'audio' && audioPreview}
|
||||
{getAssetType(imageUrlState.value) === 'video' && videoPreview}
|
||||
{getAssetType(imageUrlState.value) === 'image' && <img alt="asset-preview" src={previewUrl} />}
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
|
||||
<Conditional test={uploadMethod === 'new'}>
|
||||
<div>
|
||||
<div className="flex flex-col items-center px-8 w-full">
|
||||
<div className="flex justify-items-start mb-5 w-full font-bold">
|
||||
<div className="form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadService === 'nft-storage'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio3"
|
||||
name="inlineRadioOptions3"
|
||||
onClick={() => {
|
||||
setUploadService('nft-storage')
|
||||
}}
|
||||
type="radio"
|
||||
value="nft-storage"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio3"
|
||||
>
|
||||
Upload using NFT.Storage
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="ml-2 form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadService === 'pinata'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio4"
|
||||
name="inlineRadioOptions4"
|
||||
onClick={() => {
|
||||
setUploadService('pinata')
|
||||
}}
|
||||
type="radio"
|
||||
value="pinata"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio4"
|
||||
>
|
||||
Upload using Pinata
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full">
|
||||
<Conditional test={uploadService === 'nft-storage'}>
|
||||
<TextInput {...nftStorageApiKeyState} className="w-full" />
|
||||
</Conditional>
|
||||
<Conditional test={uploadService === 'pinata'}>
|
||||
<TextInput {...pinataApiKeyState} className="w-full" />
|
||||
<div className="w-[20px]" />
|
||||
<TextInput {...pinataSecretKeyState} className="w-full" />
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="grid grid-cols-2">
|
||||
<div>
|
||||
<div className="w-full">
|
||||
<div>
|
||||
<label
|
||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
||||
htmlFor="assetFile"
|
||||
>
|
||||
Asset Selection
|
||||
</label>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
|
||||
'rounded border-2 border-white/20 border-dashed',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
accept="image/*, audio/*, video/*, .html"
|
||||
className={clsx(
|
||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
||||
)}
|
||||
id="assetFile"
|
||||
onChange={selectAsset}
|
||||
ref={assetFileRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Conditional test={assetFile !== undefined}>
|
||||
<SingleAssetPreview
|
||||
relatedAsset={assetFile}
|
||||
subtitle={`Asset filename: ${assetFile?.name as string}`}
|
||||
/>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
99
components/openEdition/MintingDetails.tsx
Normal file
99
components/openEdition/MintingDetails.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||
import { InputDateTime } from 'components/InputDateTime'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
|
||||
import { useWallet } from '../../contexts/wallet'
|
||||
import { NumberInput, TextInput } from '../forms/FormInput'
|
||||
import type { UploadMethod } from './OffChainMetadataUploadDetails'
|
||||
|
||||
interface MintingDetailsProps {
|
||||
onChange: (data: MintingDetailsDataProps) => void
|
||||
uploadMethod: UploadMethod
|
||||
minimumMintPrice: number
|
||||
}
|
||||
|
||||
export interface MintingDetailsDataProps {
|
||||
unitPrice: string
|
||||
perAddressLimit: number
|
||||
startTime: string
|
||||
endTime: string
|
||||
paymentAddress?: string
|
||||
}
|
||||
|
||||
export const MintingDetails = ({ onChange, uploadMethod, minimumMintPrice }: MintingDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
|
||||
const [timestamp, setTimestamp] = useState<Date | undefined>()
|
||||
const [endTimestamp, setEndTimestamp] = useState<Date | undefined>()
|
||||
|
||||
const unitPriceState = useNumberInputState({
|
||||
id: 'unitPrice',
|
||||
name: 'unitPrice',
|
||||
title: 'Unit Price',
|
||||
subtitle: `Price of each token (min. ${minimumMintPrice} STARS)`,
|
||||
placeholder: '50',
|
||||
})
|
||||
|
||||
const perAddressLimitState = useNumberInputState({
|
||||
id: 'peraddresslimit',
|
||||
name: 'peraddresslimit',
|
||||
title: 'Per Address Limit',
|
||||
subtitle: '',
|
||||
placeholder: '1',
|
||||
})
|
||||
|
||||
const paymentAddressState = useInputState({
|
||||
id: 'payment-address',
|
||||
name: 'paymentAddress',
|
||||
title: 'Payment Address (optional)',
|
||||
subtitle: 'Address to receive minting revenues (defaults to current wallet address)',
|
||||
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
|
||||
})
|
||||
|
||||
const resolvePaymentAddress = async () => {
|
||||
await resolveAddress(paymentAddressState.value.trim(), wallet).then((resolvedAddress) => {
|
||||
paymentAddressState.onChange(resolvedAddress)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void resolvePaymentAddress()
|
||||
}, [paymentAddressState.value])
|
||||
|
||||
useEffect(() => {
|
||||
const data: MintingDetailsDataProps = {
|
||||
unitPrice: unitPriceState.value
|
||||
? (Number(unitPriceState.value) * 1_000_000).toString()
|
||||
: unitPriceState.value === 0
|
||||
? '0'
|
||||
: '',
|
||||
perAddressLimit: perAddressLimitState.value,
|
||||
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
|
||||
endTime: endTimestamp ? (endTimestamp.getTime() * 1_000_000).toString() : '',
|
||||
paymentAddress: paymentAddressState.value.trim(),
|
||||
}
|
||||
onChange(data)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [unitPriceState.value, perAddressLimitState.value, timestamp, endTimestamp, paymentAddressState.value])
|
||||
|
||||
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 />
|
||||
<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} />
|
||||
</FormControl>
|
||||
<FormControl htmlId="endTimestamp" isRequired subtitle="Minting end time (local)" title="End Time">
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setEndTimestamp(date)} value={endTimestamp} />
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<TextInput className="pr-4 pl-4 mt-3" {...paymentAddressState} />
|
||||
</div>
|
||||
)
|
||||
}
|
452
components/openEdition/OffChainMetadataUploadDetails.tsx
Normal file
452
components/openEdition/OffChainMetadataUploadDetails.tsx
Normal file
@ -0,0 +1,452 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
|
||||
/* eslint-disable no-misleading-character-class */
|
||||
/* eslint-disable no-control-regex */
|
||||
/* eslint-disable @typescript-eslint/no-loop-func */
|
||||
import clsx from 'clsx'
|
||||
import { Alert } from 'components/Alert'
|
||||
import { Anchor } from 'components/Anchor'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { TextInput } from 'components/forms/FormInput'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { MetadataInput } from 'components/MetadataInput'
|
||||
import { MetadataModal } from 'components/MetadataModal'
|
||||
import { SingleAssetPreview } from 'components/SingleAssetPreview'
|
||||
import { addLogItem } from 'contexts/log'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import type { UploadServiceType } from 'services/upload'
|
||||
import { uid } from 'utils/random'
|
||||
import { naturalCompare } from 'utils/sort'
|
||||
|
||||
import type { MetadataStorageMethod } from './OpenEditionMinterCreator'
|
||||
|
||||
export type UploadMethod = 'new' | 'existing'
|
||||
|
||||
interface OffChainMetadataUploadDetailsProps {
|
||||
onChange: (value: OffChainMetadataUploadDetailsDataProps) => void
|
||||
metadataStorageMethod?: MetadataStorageMethod
|
||||
}
|
||||
|
||||
export interface OffChainMetadataUploadDetailsDataProps {
|
||||
assetFiles: File[]
|
||||
metadataFiles: File[]
|
||||
uploadService: UploadServiceType
|
||||
nftStorageApiKey?: string
|
||||
pinataApiKey?: string
|
||||
pinataSecretKey?: string
|
||||
uploadMethod: UploadMethod
|
||||
tokenURI?: string
|
||||
imageUrl?: string
|
||||
openEditionMinterMetadataFile?: File
|
||||
}
|
||||
|
||||
export const OffChainMetadataUploadDetails = ({
|
||||
onChange,
|
||||
metadataStorageMethod,
|
||||
}: OffChainMetadataUploadDetailsProps) => {
|
||||
const [assetFilesArray, setAssetFilesArray] = useState<File[]>([])
|
||||
const [metadataFilesArray, setMetadataFilesArray] = useState<File[]>([])
|
||||
const [uploadMethod, setUploadMethod] = useState<UploadMethod>('new')
|
||||
const [uploadService, setUploadService] = useState<UploadServiceType>('nft-storage')
|
||||
const [metadataFileArrayIndex, setMetadataFileArrayIndex] = useState(0)
|
||||
const [refreshMetadata, setRefreshMetadata] = useState(false)
|
||||
|
||||
const [openEditionMinterMetadataFile, setOpenEditionMinterMetadataFile] = useState<File | undefined>()
|
||||
|
||||
const assetFilesRef = useRef<HTMLInputElement | null>(null)
|
||||
const metadataFilesRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const nftStorageApiKeyState = useInputState({
|
||||
id: 'nft-storage-api-key',
|
||||
name: 'nftStorageApiKey',
|
||||
title: 'NFT.Storage API Key',
|
||||
placeholder: 'Enter NFT.Storage API Key',
|
||||
defaultValue: '',
|
||||
})
|
||||
const pinataApiKeyState = useInputState({
|
||||
id: 'pinata-api-key',
|
||||
name: 'pinataApiKey',
|
||||
title: 'Pinata API Key',
|
||||
placeholder: 'Enter Pinata API Key',
|
||||
defaultValue: '',
|
||||
})
|
||||
const pinataSecretKeyState = useInputState({
|
||||
id: 'pinata-secret-key',
|
||||
name: 'pinataSecretKey',
|
||||
title: 'Pinata Secret Key',
|
||||
placeholder: 'Enter Pinata Secret Key',
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const tokenUriState = useInputState({
|
||||
id: 'tokenUri',
|
||||
name: 'tokenUri',
|
||||
title: 'Token URI',
|
||||
placeholder: 'ipfs://',
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const coverImageUrlState = useInputState({
|
||||
id: 'coverImageUrl',
|
||||
name: 'coverImageUrl',
|
||||
title: 'Cover Image URL',
|
||||
placeholder: 'ipfs://',
|
||||
defaultValue: '',
|
||||
})
|
||||
|
||||
const selectAssets = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setAssetFilesArray([])
|
||||
setMetadataFilesArray([])
|
||||
if (event.target.files === null) return
|
||||
let loadedFileCount = 0
|
||||
const files: File[] = []
|
||||
let reader: FileReader
|
||||
for (let i = 0; i < event.target.files.length; i++) {
|
||||
reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
||||
if (!event.target.files) return toast.error('No files selected.')
|
||||
const assetFile = new File([e.target.result], event.target.files[i].name, { type: 'image/jpg' })
|
||||
files.push(assetFile)
|
||||
}
|
||||
reader.readAsArrayBuffer(event.target.files[i])
|
||||
reader.onloadend = () => {
|
||||
if (!event.target.files) return toast.error('No file selected.')
|
||||
loadedFileCount++
|
||||
if (loadedFileCount === event.target.files.length) {
|
||||
setAssetFilesArray(files.sort((a, b) => naturalCompare(a.name, b.name)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setMetadataFilesArray([])
|
||||
if (event.target.files === null) return toast.error('No files selected.')
|
||||
|
||||
let loadedFileCount = 0
|
||||
const files: File[] = []
|
||||
let reader: FileReader
|
||||
for (let i = 0; i < event.target.files.length; i++) {
|
||||
reader = new FileReader()
|
||||
reader.onload = async (e) => {
|
||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
||||
if (!event.target.files) return toast.error('No files selected.')
|
||||
const metadataFile = new File([e.target.result], event.target.files[i].name, { type: 'application/json' })
|
||||
files.push(metadataFile)
|
||||
try {
|
||||
const parsedMetadata = JSON.parse(await metadataFile.text())
|
||||
if (!parsedMetadata || typeof parsedMetadata !== 'object') {
|
||||
event.target.value = ''
|
||||
setMetadataFilesArray([])
|
||||
return toast.error(`Invalid metadata file: ${metadataFile.name}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
event.target.value = ''
|
||||
setMetadataFilesArray([])
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
return toast.error(`Invalid metadata file: ${metadataFile.name}`)
|
||||
}
|
||||
}
|
||||
reader.readAsText(event.target.files[i], 'utf8')
|
||||
reader.onloadend = () => {
|
||||
if (!event.target.files) return toast.error('No file selected.')
|
||||
loadedFileCount++
|
||||
if (loadedFileCount === event.target.files.length) {
|
||||
setMetadataFilesArray(files.sort((a, b) => naturalCompare(a.name, b.name)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateMetadataFileIndex = (index: number) => {
|
||||
setMetadataFileArrayIndex(index)
|
||||
setRefreshMetadata((prev) => !prev)
|
||||
}
|
||||
|
||||
const updateMetadataFileArray = (updatedMetadataFile: File) => {
|
||||
metadataFilesArray[metadataFileArrayIndex] = updatedMetadataFile
|
||||
}
|
||||
|
||||
const updateOpenEditionMinterMetadataFile = (updatedMetadataFile: File) => {
|
||||
setOpenEditionMinterMetadataFile(updatedMetadataFile)
|
||||
console.log('Updated Open Edition Minter Metadata File:')
|
||||
console.log(openEditionMinterMetadataFile)
|
||||
}
|
||||
const regex =
|
||||
/[\0-\x1F\x7F-\x9F\xAD\u0378\u0379\u037F-\u0383\u038B\u038D\u03A2\u0528-\u0530\u0557\u0558\u0560\u0588\u058B-\u058E\u0590\u05C8-\u05CF\u05EB-\u05EF\u05F5-\u0605\u061C\u061D\u06DD\u070E\u070F\u074B\u074C\u07B2-\u07BF\u07FB-\u07FF\u082E\u082F\u083F\u085C\u085D\u085F-\u089F\u08A1\u08AD-\u08E3\u08FF\u0978\u0980\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09FC-\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF2-\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B55\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B78-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BFB-\u0C00\u0C04\u0C0D\u0C11\u0C29\u0C34\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5A-\u0C5F\u0C64\u0C65\u0C70-\u0C77\u0C80\u0C81\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0D01\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D45\u0D49\u0D4F-\u0D56\u0D58-\u0D5F\u0D64\u0D65\u0D76-\u0D78\u0D80\u0D81\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DF1\u0DF5-\u0E00\u0E3B-\u0E3E\u0E5C-\u0E80\u0E83\u0E85\u0E86\u0E89\u0E8B\u0E8C\u0E8E-\u0E93\u0E98\u0EA0\u0EA4\u0EA6\u0EA8\u0EA9\u0EAC\u0EBA\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F48\u0F6D-\u0F70\u0F98\u0FBD\u0FCD\u0FDB-\u0FFF\u10C6\u10C8-\u10CC\u10CE\u10CF\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u137D-\u137F\u139A-\u139F\u13F5-\u13FF\u169D-\u169F\u16F1-\u16FF\u170D\u1715-\u171F\u1737-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17DE\u17DF\u17EA-\u17EF\u17FA-\u17FF\u180F\u181A-\u181F\u1878-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191D-\u191F\u192C-\u192F\u193C-\u193F\u1941-\u1943\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DB-\u19DD\u1A1C\u1A1D\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1A9F\u1AAE-\u1AFF\u1B4C-\u1B4F\u1B7D-\u1B7F\u1BF4-\u1BFB\u1C38-\u1C3A\u1C4A-\u1C4C\u1C80-\u1CBF\u1CC8-\u1CCF\u1CF7-\u1CFF\u1DE7-\u1DFB\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FC5\u1FD4\u1FD5\u1FDC\u1FF0\u1FF1\u1FF5\u1FFF\u200B-\u200F\u2020-\u202E\u2060-\u206F\u2072\u2073\u208F\u209D-\u209F\u20BB-\u20CF\u20F1-\u20FF\u218A-\u218F\u23F4-\u23FF\u2427-\u243F\u244B-\u245F\u2700\u2B4D-\u2B4F\u2B5A-\u2BFF\u2C2F\u2C5F\u2CF4-\u2CF8\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D71-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E3C-\u2E7F\u2E9A\u2EF4-\u2EFF\u2FD6-\u2FEF\u2FFC-\u2FFF\u3040\u3097\u3098\u3100-\u3104\u312E-\u3130\u318F\u31BB-\u31BF\u31E4-\u31EF\u321F\u32FF\u4DB6-\u4DBF\u9FCD-\u9FFF\uA48D-\uA48F\uA4C7-\uA4CF\uA62C-\uA63F\uA698-\uA69E\uA6F8-\uA6FF\uA78F\uA794-\uA79F\uA7AB-\uA7F7\uA82C-\uA82F\uA83A-\uA83F\uA878-\uA87F\uA8C5-\uA8CD\uA8DA-\uA8DF\uA8FC-\uA8FF\uA954-\uA95E\uA97D-\uA97F\uA9CE\uA9DA-\uA9DD\uA9E0-\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A\uAA5B\uAA7C-\uAA7F\uAAC3-\uAADA\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F-\uABBF\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBC2-\uFBD2\uFD40-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFE\uFDFF\uFE1A-\uFE1F\uFE27-\uFE2F\uFE53\uFE67\uFE6C-\uFE6F\uFE75\uFEFD-\uFF00\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFDF\uFFE7\uFFEF-\uFFFB\uFFFE\uFFFF]/g
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const data: OffChainMetadataUploadDetailsDataProps = {
|
||||
assetFiles: assetFilesArray,
|
||||
metadataFiles: metadataFilesArray,
|
||||
uploadService,
|
||||
nftStorageApiKey: nftStorageApiKeyState.value,
|
||||
pinataApiKey: pinataApiKeyState.value,
|
||||
pinataSecretKey: pinataSecretKeyState.value,
|
||||
uploadMethod,
|
||||
tokenURI: tokenUriState.value
|
||||
.replace('IPFS://', 'ipfs://')
|
||||
.replace(/,/g, '')
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '')
|
||||
.replace(/ /g, '')
|
||||
.replace(regex, ''),
|
||||
imageUrl: coverImageUrlState.value
|
||||
.replace('IPFS://', 'ipfs://')
|
||||
.replace(/,/g, '')
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '')
|
||||
.replace(/ /g, '')
|
||||
.replace(regex, ''),
|
||||
openEditionMinterMetadataFile,
|
||||
}
|
||||
onChange(data)
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
}
|
||||
}, [
|
||||
assetFilesArray,
|
||||
metadataFilesArray,
|
||||
uploadService,
|
||||
nftStorageApiKeyState.value,
|
||||
pinataApiKeyState.value,
|
||||
pinataSecretKeyState.value,
|
||||
uploadMethod,
|
||||
tokenUriState.value,
|
||||
coverImageUrlState.value,
|
||||
refreshMetadata,
|
||||
openEditionMinterMetadataFile,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (metadataFilesRef.current) metadataFilesRef.current.value = ''
|
||||
setMetadataFilesArray([])
|
||||
if (assetFilesRef.current) assetFilesRef.current.value = ''
|
||||
setAssetFilesArray([])
|
||||
tokenUriState.onChange('')
|
||||
coverImageUrlState.onChange('')
|
||||
}, [uploadMethod, metadataStorageMethod])
|
||||
|
||||
return (
|
||||
<div className="justify-items-start mb-3 rounded border-2 border-white/20 flex-column">
|
||||
<div className="flex justify-center">
|
||||
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadMethod === 'new'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio2"
|
||||
name="inlineRadioOptions2"
|
||||
onClick={() => {
|
||||
setUploadMethod('new')
|
||||
}}
|
||||
type="radio"
|
||||
value="New"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio2"
|
||||
>
|
||||
Upload asset & metadata
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadMethod === 'existing'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio1"
|
||||
name="inlineRadioOptions1"
|
||||
onClick={() => {
|
||||
setUploadMethod('existing')
|
||||
}}
|
||||
type="radio"
|
||||
value="Existing"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio1"
|
||||
>
|
||||
Use an existing Token URI
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 py-5 pb-8">
|
||||
<Conditional test={uploadMethod === 'existing'}>
|
||||
<div className="ml-3 flex-column">
|
||||
<p className="mb-5 ml-5">
|
||||
Though Stargaze's sg721 contract allows for off-chain metadata storage, it is recommended to use a
|
||||
decentralized storage solution, such as IPFS. <br /> You may head over to{' '}
|
||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://nft.storage">
|
||||
NFT.Storage
|
||||
</Anchor>{' '}
|
||||
or{' '}
|
||||
<Anchor className="font-bold text-plumbus hover:underline" href="https://www.pinata.cloud/">
|
||||
Pinata
|
||||
</Anchor>{' '}
|
||||
and upload your asset & metadata manually to get a URI for your token before minting.
|
||||
</p>
|
||||
<div>
|
||||
<TextInput {...tokenUriState} className="ml-4 w-1/2" />
|
||||
<TextInput {...coverImageUrlState} className="mt-2 ml-4 w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
<Conditional test={uploadMethod === 'new'}>
|
||||
<div>
|
||||
<div className="flex flex-col items-center px-8 w-full">
|
||||
<div className="flex justify-items-start mb-5 w-full font-bold">
|
||||
<div className="form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadService === 'nft-storage'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio3"
|
||||
name="inlineRadioOptions3"
|
||||
onClick={() => {
|
||||
setUploadService('nft-storage')
|
||||
}}
|
||||
type="radio"
|
||||
value="nft-storage"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio3"
|
||||
>
|
||||
Upload using NFT.Storage
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="ml-2 form-check form-check-inline">
|
||||
<input
|
||||
checked={uploadService === 'pinata'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio4"
|
||||
name="inlineRadioOptions4"
|
||||
onClick={() => {
|
||||
setUploadService('pinata')
|
||||
}}
|
||||
type="radio"
|
||||
value="pinata"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio4"
|
||||
>
|
||||
Upload using Pinata
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full">
|
||||
<Conditional test={uploadService === 'nft-storage'}>
|
||||
<TextInput {...nftStorageApiKeyState} className="w-full" />
|
||||
</Conditional>
|
||||
<Conditional test={uploadService === 'pinata'}>
|
||||
<TextInput {...pinataApiKeyState} className="w-full" />
|
||||
<div className="w-[20px]" />
|
||||
<TextInput {...pinataSecretKeyState} className="w-full" />
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="w-full">
|
||||
<Conditional
|
||||
test={
|
||||
assetFilesArray.length > 0 &&
|
||||
metadataFilesArray.length > 0 &&
|
||||
assetFilesArray.length !== metadataFilesArray.length
|
||||
}
|
||||
>
|
||||
<Alert className="mt-4 ml-8 w-3/4" type="warning">
|
||||
The number of assets and metadata files should match.
|
||||
</Alert>
|
||||
</Conditional>
|
||||
|
||||
<div>
|
||||
<label
|
||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
||||
htmlFor="assetFiles"
|
||||
>
|
||||
Asset Selection
|
||||
</label>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
|
||||
'rounded border-2 border-white/20 border-dashed',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
accept="image/*, audio/*, video/*, .html"
|
||||
className={clsx(
|
||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
||||
)}
|
||||
id="assetFiles"
|
||||
onChange={selectAssets}
|
||||
ref={assetFilesRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{assetFilesArray.length > 0 && (
|
||||
<div>
|
||||
<label
|
||||
className="block mt-5 mr-1 mb-1 ml-8 w-full font-bold text-white dark:text-gray-300"
|
||||
htmlFor="metadataFiles"
|
||||
>
|
||||
Metadata Selection (optional)
|
||||
</label>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex relative justify-center items-center mx-8 mt-2 space-y-4 w-full h-32',
|
||||
'rounded border-2 border-white/20 border-dashed',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
accept="application/json"
|
||||
className={clsx(
|
||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
||||
)}
|
||||
id="metadataFiles"
|
||||
onChange={selectMetadata}
|
||||
ref={metadataFilesRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Conditional test={assetFilesArray.length >= 1}>
|
||||
<MetadataModal
|
||||
assetFile={assetFilesArray[metadataFileArrayIndex]}
|
||||
metadataFile={metadataFilesArray[metadataFileArrayIndex]}
|
||||
refresher={refreshMetadata}
|
||||
updateMetadata={updateMetadataFileArray}
|
||||
/>
|
||||
</Conditional>
|
||||
</div>
|
||||
<SingleAssetPreview
|
||||
relatedAsset={assetFilesArray[0]}
|
||||
subtitle={`Asset filename: ${assetFilesArray[0]?.name}`}
|
||||
updateMetadataFileIndex={updateMetadataFileIndex}
|
||||
/>
|
||||
</div>
|
||||
<MetadataInput
|
||||
selectedAssetFile={assetFilesArray[0]}
|
||||
selectedMetadataFile={metadataFilesArray[0]}
|
||||
updateMetadataToUpload={updateOpenEditionMinterMetadataFile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
260
components/openEdition/OnChainMetadataInputDetails.tsx
Normal file
260
components/openEdition/OnChainMetadataInputDetails.tsx
Normal file
@ -0,0 +1,260 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { useMetadataAttributesState } from 'components/forms/MetadataAttributes.hooks'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { Trait } from 'contracts/badgeHub'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { TextInput } from '../forms/FormInput'
|
||||
import { MetadataAttributes } from '../forms/MetadataAttributes'
|
||||
import { Tooltip } from '../Tooltip'
|
||||
import type { UploadMethod } from './ImageUploadDetails'
|
||||
|
||||
interface OnChainMetadataInputDetailsProps {
|
||||
onChange: (data: OnChainMetadataInputDetailsDataProps) => void
|
||||
uploadMethod: UploadMethod | undefined
|
||||
}
|
||||
|
||||
export interface OnChainMetadataInputDetailsDataProps {
|
||||
name?: string
|
||||
description?: string
|
||||
attributes?: Trait[]
|
||||
image_data?: string
|
||||
external_url?: string
|
||||
background_color?: string
|
||||
animation_url?: string
|
||||
youtube_url?: string
|
||||
}
|
||||
|
||||
export const OnChainMetadataInputDetails = ({ onChange, uploadMethod }: OnChainMetadataInputDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
||||
const [metadataFile, setMetadataFile] = useState<File>()
|
||||
const [metadataFeeRate, setMetadataFeeRate] = useState<number>(0)
|
||||
|
||||
const metadataFileRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const nameState = useInputState({
|
||||
id: 'name',
|
||||
name: 'name',
|
||||
title: 'Name',
|
||||
placeholder: 'My Awesome Collection',
|
||||
})
|
||||
|
||||
const descriptionState = useInputState({
|
||||
id: 'description',
|
||||
name: 'description',
|
||||
title: 'Description',
|
||||
placeholder: 'My Awesome Collection Description',
|
||||
})
|
||||
|
||||
const imageDataState = useInputState({
|
||||
id: 'metadata-image-data',
|
||||
name: 'metadata-image-data',
|
||||
title: 'Image Data',
|
||||
subtitle: 'Raw SVG image data',
|
||||
})
|
||||
|
||||
const externalUrlState = useInputState({
|
||||
id: 'metadata-external-url',
|
||||
name: 'metadata-external-url',
|
||||
title: 'External URL',
|
||||
subtitle: 'External URL for the token',
|
||||
placeholder: 'https://',
|
||||
})
|
||||
|
||||
const attributesState = useMetadataAttributesState()
|
||||
|
||||
const animationUrlState = useInputState({
|
||||
id: 'metadata-animation-url',
|
||||
name: 'metadata-animation-url',
|
||||
title: 'Animation URL',
|
||||
subtitle: 'Animation URL for the token',
|
||||
placeholder: 'https://',
|
||||
})
|
||||
|
||||
const youtubeUrlState = useInputState({
|
||||
id: 'metadata-youtube-url',
|
||||
name: 'metadata-youtube-url',
|
||||
title: 'YouTube URL',
|
||||
subtitle: 'YouTube URL for the token',
|
||||
placeholder: 'https://',
|
||||
})
|
||||
|
||||
const parseMetadata = async () => {
|
||||
try {
|
||||
let parsedMetadata: any
|
||||
if (metadataFile) {
|
||||
attributesState.reset()
|
||||
parsedMetadata = JSON.parse(await metadataFile.text())
|
||||
|
||||
if (!parsedMetadata.attributes || parsedMetadata.attributes.length === 0) {
|
||||
attributesState.add({
|
||||
trait_type: '',
|
||||
value: '',
|
||||
})
|
||||
} else {
|
||||
for (let i = 0; i < parsedMetadata.attributes.length; i++) {
|
||||
attributesState.add({
|
||||
trait_type: parsedMetadata.attributes[i].trait_type,
|
||||
value: parsedMetadata.attributes[i].value,
|
||||
})
|
||||
}
|
||||
}
|
||||
nameState.onChange(parsedMetadata.name ? parsedMetadata.name : '')
|
||||
descriptionState.onChange(parsedMetadata.description ? parsedMetadata.description : '')
|
||||
externalUrlState.onChange(parsedMetadata.external_url ? parsedMetadata.external_url : '')
|
||||
youtubeUrlState.onChange(parsedMetadata.youtube_url ? parsedMetadata.youtube_url : '')
|
||||
animationUrlState.onChange(parsedMetadata.animation_url ? parsedMetadata.animation_url : '')
|
||||
imageDataState.onChange(parsedMetadata.image_data ? parsedMetadata.image_data : '')
|
||||
} else {
|
||||
attributesState.reset()
|
||||
nameState.onChange('')
|
||||
descriptionState.onChange('')
|
||||
externalUrlState.onChange('')
|
||||
youtubeUrlState.onChange('')
|
||||
animationUrlState.onChange('')
|
||||
imageDataState.onChange('')
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Error parsing metadata file: Invalid JSON format.')
|
||||
if (metadataFileRef.current) metadataFileRef.current.value = ''
|
||||
setMetadataFile(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const selectMetadata = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setMetadataFile(undefined)
|
||||
if (event.target.files === null) return
|
||||
|
||||
let selectedFile: File
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
if (!event.target.files) return toast.error('No file selected.')
|
||||
if (!e.target?.result) return toast.error('Error parsing file.')
|
||||
selectedFile = new File([e.target.result], event.target.files[0].name, { type: 'application/json' })
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (event.target.files[0]) reader.readAsArrayBuffer(event.target.files[0])
|
||||
else return toast.error('No file selected.')
|
||||
reader.onloadend = () => {
|
||||
if (!event.target.files) return toast.error('No file selected.')
|
||||
setMetadataFile(selectedFile)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void parseMetadata()
|
||||
if (!metadataFile)
|
||||
attributesState.add({
|
||||
trait_type: '',
|
||||
value: '',
|
||||
})
|
||||
}, [metadataFile])
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const data: OnChainMetadataInputDetailsDataProps = {
|
||||
name: nameState.value || undefined,
|
||||
description: descriptionState.value || undefined,
|
||||
attributes:
|
||||
attributesState.values[0]?.trait_type && attributesState.values[0]?.value
|
||||
? attributesState.values
|
||||
.map((attr) => ({
|
||||
trait_type: attr.trait_type,
|
||||
value: attr.value,
|
||||
}))
|
||||
.filter((attr) => attr.trait_type && attr.value)
|
||||
: undefined,
|
||||
image_data: imageDataState.value || undefined,
|
||||
external_url: externalUrlState.value || undefined,
|
||||
animation_url: animationUrlState.value.trim() || undefined,
|
||||
youtube_url: youtubeUrlState.value || undefined,
|
||||
}
|
||||
onChange(data)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
nameState.value,
|
||||
descriptionState.value,
|
||||
timestamp,
|
||||
imageDataState.value,
|
||||
externalUrlState.value,
|
||||
attributesState.values,
|
||||
animationUrlState.value,
|
||||
youtubeUrlState.value,
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="py-3 px-8 rounded border-2 border-white/20">
|
||||
<span className="ml-4 text-xl font-bold underline underline-offset-4">NFT Metadata</span>
|
||||
<div className={clsx('grid grid-cols-2 mt-4 mb-2 ml-4 max-w-6xl')}>
|
||||
<div className={clsx('mt-6')}>
|
||||
<TextInput className="mt-2" {...nameState} />
|
||||
<TextInput className="mt-2" {...descriptionState} />
|
||||
<TextInput className="mt-2" {...externalUrlState} />
|
||||
<Conditional test={uploadMethod === 'existing'}>
|
||||
<TextInput className="mt-2" {...animationUrlState} />
|
||||
</Conditional>
|
||||
<TextInput className="mt-2" {...youtubeUrlState} />
|
||||
</div>
|
||||
<div className={clsx('ml-10')}>
|
||||
<div>
|
||||
<MetadataAttributes
|
||||
attributes={attributesState.entries}
|
||||
onAdd={attributesState.add}
|
||||
onChange={attributesState.update}
|
||||
onRemove={attributesState.remove}
|
||||
title="Traits"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Tooltip
|
||||
backgroundColor="bg-blue-500"
|
||||
label="A metadata file can be selected to automatically fill in the related fields."
|
||||
placement="bottom"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
className="block mt-2 mr-1 mb-1 w-full font-bold text-white dark:text-gray-300"
|
||||
htmlFor="assetFile"
|
||||
>
|
||||
Metadata File Selection (optional)
|
||||
</label>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex relative justify-center items-center mt-2 space-y-4 w-full h-32',
|
||||
'rounded border-2 border-white/20 border-dashed',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
accept="application/json"
|
||||
className={clsx(
|
||||
'file:py-2 file:px-4 file:mr-4 file:bg-plumbus-light file:rounded file:border-0 cursor-pointer',
|
||||
'before:absolute before:inset-0 before:hover:bg-white/5 before:transition',
|
||||
)}
|
||||
id="metadataFile"
|
||||
onChange={selectMetadata}
|
||||
ref={metadataFileRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
682
components/openEdition/OpenEditionMinterCreator.tsx
Normal file
682
components/openEdition/OpenEditionMinterCreator.tsx
Normal file
@ -0,0 +1,682 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable no-nested-ternary */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import { coin } from '@cosmjs/proto-signing'
|
||||
import clsx from 'clsx'
|
||||
import { Button } from 'components/Button'
|
||||
import type { MinterType } from 'components/collections/actions/Combobox'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { ConfirmationModal } from 'components/ConfirmationModal'
|
||||
import { LoadingModal } from 'components/LoadingModal'
|
||||
import { useContracts } from 'contexts/contracts'
|
||||
import { addLogItem } from 'contexts/log'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { DispatchExecuteArgs as OpenEditionFactoryDispatchExecuteArgs } from 'contracts/openEditionFactory/messages/execute'
|
||||
import { dispatchExecute as openEditionFactoryDispatchExecute } from 'contracts/openEditionFactory/messages/execute'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { upload } from 'services/upload'
|
||||
import {
|
||||
OPEN_EDITION_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS,
|
||||
SG721_CODE_ID,
|
||||
SG721_UPDATABLE_CODE_ID,
|
||||
} from 'utils/constants'
|
||||
import { getAssetType } from 'utils/getAssetType'
|
||||
import { isValidAddress } from 'utils/isValidAddress'
|
||||
import { uid } from 'utils/random'
|
||||
|
||||
import { type CollectionDetailsDataProps, CollectionDetails } from './CollectionDetails'
|
||||
import type { ImageUploadDetailsDataProps } from './ImageUploadDetails'
|
||||
import { ImageUploadDetails } from './ImageUploadDetails'
|
||||
import type { MintingDetailsDataProps } from './MintingDetails'
|
||||
import { MintingDetails } from './MintingDetails'
|
||||
import type { UploadMethod } from './OffChainMetadataUploadDetails'
|
||||
import {
|
||||
type OffChainMetadataUploadDetailsDataProps,
|
||||
OffChainMetadataUploadDetails,
|
||||
} from './OffChainMetadataUploadDetails'
|
||||
import type { OnChainMetadataInputDetailsDataProps } from './OnChainMetadataInputDetails'
|
||||
import { OnChainMetadataInputDetails } from './OnChainMetadataInputDetails'
|
||||
import { type RoyaltyDetailsDataProps, RoyaltyDetails } from './RoyaltyDetails'
|
||||
|
||||
export type MetadataStorageMethod = 'off-chain' | 'on-chain'
|
||||
|
||||
interface OpenEditionMinterCreatorProps {
|
||||
onChange: (data: OpenEditionMinterCreatorDataProps) => void
|
||||
openEditionMinterUpdatableCreationFee?: string
|
||||
openEditionMinterCreationFee?: string
|
||||
minimumMintPrice?: string
|
||||
minimumUpdatableMintPrice?: string
|
||||
minterType?: MinterType
|
||||
}
|
||||
|
||||
export interface OpenEditionMinterCreatorDataProps {
|
||||
metadataStorageMethod: MetadataStorageMethod
|
||||
openEditionMinterContractAddress: string | null
|
||||
sg721ContractAddress: string | null
|
||||
transactionHash: string | null
|
||||
}
|
||||
|
||||
export const OpenEditionMinterCreator = ({
|
||||
onChange,
|
||||
openEditionMinterCreationFee,
|
||||
openEditionMinterUpdatableCreationFee,
|
||||
minimumMintPrice,
|
||||
minimumUpdatableMintPrice,
|
||||
minterType,
|
||||
}: 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)
|
||||
const [royaltyDetails, setRoyaltyDetails] = useState<RoyaltyDetailsDataProps | null>(null)
|
||||
const [onChainMetadataInputDetails, setOnChainMetadataInputDetails] =
|
||||
useState<OnChainMetadataInputDetailsDataProps | null>(null)
|
||||
const [offChainMetadataUploadDetails, setOffChainMetadataUploadDetails] =
|
||||
useState<OffChainMetadataUploadDetailsDataProps | null>(null)
|
||||
const [mintingDetails, setMintingDetails] = useState<MintingDetailsDataProps | null>(null)
|
||||
|
||||
const [creationInProgress, setCreationInProgress] = useState(false)
|
||||
const [readyToCreate, setReadyToCreate] = useState(false)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [tokenUri, setTokenUri] = useState<string | null>(null)
|
||||
const [tokenImageUri, setTokenImageUri] = useState<string | null>(null)
|
||||
const [coverImageUrl, setCoverImageUrl] = useState<string | null>(null)
|
||||
const [openEditionMinterContractAddress, setOpenEditionMinterContractAddress] = useState<string | null>(null)
|
||||
const [sg721ContractAddress, setSg721ContractAddress] = useState<string | null>(null)
|
||||
const [transactionHash, setTransactionHash] = useState<string | null>(null)
|
||||
|
||||
const performOpenEditionMinterChecks = () => {
|
||||
try {
|
||||
setReadyToCreate(false)
|
||||
checkUploadDetails()
|
||||
checkCollectionDetails()
|
||||
checkMintingDetails()
|
||||
void checkRoyaltyDetails()
|
||||
.then(() => {
|
||||
void checkwalletBalance()
|
||||
.then(() => {
|
||||
setReadyToCreate(true)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toast.error(`Error in Wallet Balance: ${error.message}`, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
setReadyToCreate(false)
|
||||
})
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toast.error(`Error in Royalty Details: ${error.message}`, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
setReadyToCreate(false)
|
||||
})
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
setUploading(false)
|
||||
setReadyToCreate(false)
|
||||
}
|
||||
}
|
||||
|
||||
const checkUploadDetails = () => {
|
||||
if (!wallet.initialized) throw new Error('Wallet not connected.')
|
||||
if (
|
||||
(metadataStorageMethod === 'off-chain' && !offChainMetadataUploadDetails) ||
|
||||
(metadataStorageMethod === 'on-chain' && !imageUploadDetails)
|
||||
) {
|
||||
throw new Error('Please select assets and metadata')
|
||||
}
|
||||
|
||||
if (
|
||||
metadataStorageMethod === 'off-chain' &&
|
||||
offChainMetadataUploadDetails?.uploadMethod === 'new' &&
|
||||
offChainMetadataUploadDetails.assetFiles.length === 0
|
||||
) {
|
||||
throw new Error('Please select the asset file')
|
||||
}
|
||||
if (
|
||||
metadataStorageMethod === 'on-chain' &&
|
||||
imageUploadDetails?.uploadMethod === 'new' &&
|
||||
imageUploadDetails.assetFile === undefined
|
||||
) {
|
||||
throw new Error('Please select the asset file')
|
||||
}
|
||||
if (metadataStorageMethod === 'off-chain' && offChainMetadataUploadDetails?.uploadMethod === 'new') {
|
||||
if (
|
||||
offChainMetadataUploadDetails.uploadService === 'nft-storage' &&
|
||||
offChainMetadataUploadDetails.nftStorageApiKey === ''
|
||||
) {
|
||||
throw new Error('Please enter a valid NFT.Storage API key')
|
||||
} else if (
|
||||
offChainMetadataUploadDetails.uploadService === 'pinata' &&
|
||||
(offChainMetadataUploadDetails.pinataApiKey === '' || offChainMetadataUploadDetails.pinataSecretKey === '')
|
||||
) {
|
||||
throw new Error('Please enter valid Pinata API and secret keys')
|
||||
}
|
||||
}
|
||||
if (metadataStorageMethod === 'on-chain' && imageUploadDetails?.uploadMethod === 'new') {
|
||||
if (imageUploadDetails.uploadService === 'nft-storage' && imageUploadDetails.nftStorageApiKey === '') {
|
||||
throw new Error('Please enter a valid NFT.Storage API key')
|
||||
} else if (
|
||||
imageUploadDetails.uploadService === 'pinata' &&
|
||||
(imageUploadDetails.pinataApiKey === '' || imageUploadDetails.pinataSecretKey === '')
|
||||
) {
|
||||
throw new Error('Please enter valid Pinata API and secret keys')
|
||||
}
|
||||
}
|
||||
if (metadataStorageMethod === 'off-chain' && offChainMetadataUploadDetails?.uploadMethod === 'existing') {
|
||||
if (
|
||||
offChainMetadataUploadDetails.tokenURI === '' ||
|
||||
!(offChainMetadataUploadDetails.tokenURI as string).includes('ipfs://')
|
||||
) {
|
||||
throw new Error('Please enter a valid token URI')
|
||||
}
|
||||
if (
|
||||
offChainMetadataUploadDetails.imageUrl === '' ||
|
||||
!(offChainMetadataUploadDetails.imageUrl as string).includes('ipfs://')
|
||||
) {
|
||||
throw new Error('Please enter a valid image URI')
|
||||
}
|
||||
}
|
||||
if (metadataStorageMethod === 'on-chain' && imageUploadDetails?.uploadMethod === 'existing') {
|
||||
if (imageUploadDetails.imageUrl === '' || !(imageUploadDetails.imageUrl as string).includes('ipfs://')) {
|
||||
throw new Error('Please enter a valid asset URI')
|
||||
}
|
||||
if (
|
||||
imageUploadDetails.coverImageUrl === '' ||
|
||||
!(imageUploadDetails.coverImageUrl as string).includes('ipfs://')
|
||||
) {
|
||||
throw new Error('Please enter a valid cover image URL')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const checkCollectionDetails = () => {
|
||||
if (!collectionDetails) throw new Error('Please fill out the collection details')
|
||||
if (collectionDetails.name === '') throw new Error('Collection name is required')
|
||||
if (collectionDetails.description === '') throw new Error('Collection description is required')
|
||||
if (collectionDetails.symbol === '') throw new Error('Collection symbol is required')
|
||||
if (collectionDetails.description.length > 512)
|
||||
throw new Error('Collection description cannot exceed 512 characters')
|
||||
if (
|
||||
metadataStorageMethod === 'off-chain' &&
|
||||
offChainMetadataUploadDetails?.uploadMethod === 'new' &&
|
||||
collectionDetails.imageFile.length === 0
|
||||
)
|
||||
throw new Error('Collection cover image is required')
|
||||
if (
|
||||
metadataStorageMethod === 'on-chain' &&
|
||||
imageUploadDetails?.uploadMethod === 'new' &&
|
||||
collectionDetails.imageFile.length === 0
|
||||
)
|
||||
throw new Error('Collection cover image is required')
|
||||
if (
|
||||
collectionDetails.startTradingTime &&
|
||||
Number(collectionDetails.startTradingTime) < new Date().getTime() * 1000000
|
||||
)
|
||||
throw new Error('Invalid trading start time')
|
||||
if (
|
||||
collectionDetails.startTradingTime &&
|
||||
Number(collectionDetails.startTradingTime) < Number(mintingDetails?.startTime)
|
||||
)
|
||||
throw new Error('Trading start time must be after minting start time')
|
||||
if (collectionDetails.externalLink) {
|
||||
try {
|
||||
const url = new URL(collectionDetails.externalLink)
|
||||
} catch (e: any) {
|
||||
throw new Error(`Invalid external link: Make sure to include the protocol (e.g. https://)`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const checkMintingDetails = () => {
|
||||
if (!mintingDetails) throw new Error('Please fill out the minting details')
|
||||
if (mintingDetails.unitPrice === '') throw new Error('Mint price is required')
|
||||
if (collectionDetails?.updatable) {
|
||||
if (Number(mintingDetails.unitPrice) < Number(minimumUpdatableMintPrice))
|
||||
throw new Error(
|
||||
`Invalid mint price: The minimum mint price is ${Number(minimumUpdatableMintPrice) / 1000000} STARS`,
|
||||
)
|
||||
} else if (Number(mintingDetails.unitPrice) < Number(minimumMintPrice))
|
||||
throw new Error(`Invalid mint price: The minimum mint price is ${Number(minimumMintPrice) / 1000000} STARS`)
|
||||
if (!mintingDetails.perAddressLimit || mintingDetails.perAddressLimit < 1 || mintingDetails.perAddressLimit > 50)
|
||||
throw new Error('Invalid limit for tokens per address')
|
||||
if (mintingDetails.startTime === '') throw new Error('Start time is required')
|
||||
if (Number(mintingDetails.startTime) < new Date().getTime() * 1000000) throw new Error('Invalid start time')
|
||||
if (
|
||||
mintingDetails.paymentAddress &&
|
||||
(!isValidAddress(mintingDetails.paymentAddress) || !mintingDetails.paymentAddress.startsWith('stars1'))
|
||||
)
|
||||
throw new Error('Invalid payment address')
|
||||
}
|
||||
|
||||
const checkRoyaltyDetails = async () => {
|
||||
if (!royaltyDetails) throw new Error('Please fill out the royalty details')
|
||||
if (royaltyDetails.royaltyType === 'new') {
|
||||
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')
|
||||
if (!isValidAddress(royaltyDetails.paymentAddress.trim())) {
|
||||
if (royaltyDetails.paymentAddress.trim().endsWith('.stars')) {
|
||||
throw new Error('Royalty payment address could not be resolved')
|
||||
}
|
||||
throw new Error('Invalid royalty payment address')
|
||||
}
|
||||
const contractInfoResponse = await wallet.client
|
||||
?.queryContractRaw(
|
||||
royaltyDetails.paymentAddress.trim(),
|
||||
toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()),
|
||||
)
|
||||
.catch((e) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
if (e.message.includes('bech32')) throw new Error('Invalid royalty payment address.')
|
||||
console.log(e.message)
|
||||
})
|
||||
if (contractInfoResponse !== undefined) {
|
||||
const contractInfo = JSON.parse(new TextDecoder().decode(contractInfoResponse as Uint8Array))
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
if (contractInfo && !contractInfo.contract.includes('splits'))
|
||||
throw new Error('The provided royalty payment address does not belong to a splits contract.')
|
||||
else console.log(contractInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const checkwalletBalance = async () => {
|
||||
if (!wallet.initialized) throw new Error('Wallet not connected.')
|
||||
const amountNeeded = collectionDetails?.updatable
|
||||
? Number(openEditionMinterUpdatableCreationFee)
|
||||
: Number(openEditionMinterCreationFee)
|
||||
await wallet.client?.getBalance(wallet.address, 'ustars').then((balance) => {
|
||||
if (amountNeeded >= Number(balance.amount))
|
||||
throw new Error(
|
||||
`Insufficient wallet balance to instantiate the required contracts. Needed amount: ${(
|
||||
amountNeeded / 1000000
|
||||
).toString()} STARS`,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const createOpenEditionMinter = async () => {
|
||||
try {
|
||||
setCreationInProgress(true)
|
||||
setTokenUri(null)
|
||||
setCoverImageUrl(null)
|
||||
setTokenImageUri(null)
|
||||
setOpenEditionMinterContractAddress(null)
|
||||
setSg721ContractAddress(null)
|
||||
setTransactionHash(null)
|
||||
if (metadataStorageMethod === 'off-chain') {
|
||||
if (offChainMetadataUploadDetails?.uploadMethod === 'new') {
|
||||
setUploading(true)
|
||||
const metadataUri = await uploadForOffChainStorage()
|
||||
const coverImageUri = await upload(
|
||||
collectionDetails?.imageFile as File[],
|
||||
offChainMetadataUploadDetails.uploadService,
|
||||
'cover',
|
||||
offChainMetadataUploadDetails.nftStorageApiKey as string,
|
||||
offChainMetadataUploadDetails.pinataApiKey as string,
|
||||
offChainMetadataUploadDetails.pinataSecretKey as string,
|
||||
)
|
||||
const metadataUriWithBase = `ipfs://${metadataUri}/${(
|
||||
offChainMetadataUploadDetails.openEditionMinterMetadataFile as File
|
||||
).name.substring(
|
||||
0,
|
||||
(offChainMetadataUploadDetails.openEditionMinterMetadataFile as File).name.lastIndexOf('.'),
|
||||
)}`
|
||||
const coverImageUriWithBase = `ipfs://${coverImageUri}/${(collectionDetails?.imageFile as File[])[0].name}`
|
||||
|
||||
setTokenUri(metadataUriWithBase)
|
||||
setCoverImageUrl(coverImageUriWithBase)
|
||||
setUploading(false)
|
||||
await instantiateOpenEditionMinter(metadataUriWithBase, coverImageUriWithBase)
|
||||
} else {
|
||||
setTokenUri(offChainMetadataUploadDetails?.tokenURI as string)
|
||||
setCoverImageUrl(offChainMetadataUploadDetails?.imageUrl as string)
|
||||
await instantiateOpenEditionMinter(
|
||||
offChainMetadataUploadDetails?.tokenURI as string,
|
||||
offChainMetadataUploadDetails?.imageUrl as string,
|
||||
)
|
||||
}
|
||||
} else if (metadataStorageMethod === 'on-chain') {
|
||||
if (imageUploadDetails?.uploadMethod === 'new') {
|
||||
setUploading(true)
|
||||
const imageUri = await upload(
|
||||
[imageUploadDetails.assetFile as File],
|
||||
imageUploadDetails.uploadService,
|
||||
'cover',
|
||||
imageUploadDetails.nftStorageApiKey as string,
|
||||
imageUploadDetails.pinataApiKey as string,
|
||||
imageUploadDetails.pinataSecretKey as string,
|
||||
)
|
||||
const imageUriWithBase = `ipfs://${imageUri}/${(imageUploadDetails.assetFile as File).name}`
|
||||
setTokenImageUri(imageUriWithBase)
|
||||
|
||||
const coverImageUri = await upload(
|
||||
collectionDetails?.imageFile as File[],
|
||||
imageUploadDetails.uploadService,
|
||||
'cover',
|
||||
imageUploadDetails.nftStorageApiKey as string,
|
||||
imageUploadDetails.pinataApiKey as string,
|
||||
imageUploadDetails.pinataSecretKey as string,
|
||||
)
|
||||
const coverImageUriWithBase = `ipfs://${coverImageUri}/${(collectionDetails?.imageFile as File[])[0].name}`
|
||||
setCoverImageUrl(coverImageUriWithBase)
|
||||
|
||||
setUploading(false)
|
||||
await instantiateOpenEditionMinter(imageUriWithBase, coverImageUriWithBase)
|
||||
} else if (imageUploadDetails?.uploadMethod === 'existing') {
|
||||
setTokenImageUri(imageUploadDetails.imageUrl as string)
|
||||
setCoverImageUrl(imageUploadDetails.coverImageUrl as string)
|
||||
await instantiateOpenEditionMinter(
|
||||
imageUploadDetails.imageUrl as string,
|
||||
imageUploadDetails.coverImageUrl as string,
|
||||
)
|
||||
}
|
||||
}
|
||||
setCreationInProgress(false)
|
||||
setReadyToCreate(false)
|
||||
} catch (error: any) {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' }, duration: 10000 })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
setReadyToCreate(false)
|
||||
setCreationInProgress(false)
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const uploadForOffChainStorage = async (): Promise<string> => {
|
||||
if (!offChainMetadataUploadDetails) throw new Error('Please select the asset and fill in the metadata')
|
||||
return new Promise((resolve, reject) => {
|
||||
upload(
|
||||
offChainMetadataUploadDetails.assetFiles,
|
||||
offChainMetadataUploadDetails.uploadService,
|
||||
'assets',
|
||||
offChainMetadataUploadDetails.nftStorageApiKey as string,
|
||||
offChainMetadataUploadDetails.pinataApiKey as string,
|
||||
offChainMetadataUploadDetails.pinataSecretKey as string,
|
||||
)
|
||||
.then((assetUri: string) => {
|
||||
const fileArray: File[] = []
|
||||
const reader: FileReader = new FileReader()
|
||||
|
||||
reader.onload = (e) => {
|
||||
const data: any = JSON.parse(e.target?.result as string)
|
||||
|
||||
if (
|
||||
getAssetType(offChainMetadataUploadDetails.assetFiles[0].name) === 'audio' ||
|
||||
getAssetType(offChainMetadataUploadDetails.assetFiles[0].name) === 'video'
|
||||
) {
|
||||
data.animation_url = `ipfs://${assetUri}/${offChainMetadataUploadDetails.assetFiles[0].name}`
|
||||
}
|
||||
|
||||
data.image = `ipfs://${assetUri}/${offChainMetadataUploadDetails.assetFiles[0].name}`
|
||||
|
||||
const metadataFileBlob = new Blob([JSON.stringify(data)], {
|
||||
type: 'application/json',
|
||||
})
|
||||
|
||||
console.log('Name: ', (offChainMetadataUploadDetails.openEditionMinterMetadataFile as File).name)
|
||||
const updatedMetadataFile = new File(
|
||||
[metadataFileBlob],
|
||||
(offChainMetadataUploadDetails.openEditionMinterMetadataFile as File).name.substring(
|
||||
0,
|
||||
(offChainMetadataUploadDetails.openEditionMinterMetadataFile as File).name.lastIndexOf('.'),
|
||||
),
|
||||
{
|
||||
type: 'application/json',
|
||||
},
|
||||
)
|
||||
|
||||
fileArray.push(updatedMetadataFile)
|
||||
}
|
||||
reader.onloadend = () => {
|
||||
upload(
|
||||
fileArray,
|
||||
offChainMetadataUploadDetails.uploadService,
|
||||
'metadata',
|
||||
offChainMetadataUploadDetails.nftStorageApiKey as string,
|
||||
offChainMetadataUploadDetails.pinataApiKey as string,
|
||||
offChainMetadataUploadDetails.pinataSecretKey as string,
|
||||
)
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
}
|
||||
console.log('File: ', offChainMetadataUploadDetails.openEditionMinterMetadataFile)
|
||||
reader.readAsText(offChainMetadataUploadDetails.openEditionMinterMetadataFile as File, 'utf8')
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
}
|
||||
|
||||
const instantiateOpenEditionMinter = async (uri: string, coverImageUri: string) => {
|
||||
if (!wallet.initialized) throw new Error('Wallet not connected')
|
||||
if (!openEditionFactoryContract) throw new Error('Contract not found')
|
||||
if (!openEditionMinterContract) throw new Error('Contract not found')
|
||||
|
||||
let royaltyInfo = null
|
||||
if (royaltyDetails?.royaltyType === 'new') {
|
||||
royaltyInfo = {
|
||||
payment_address: royaltyDetails.paymentAddress.trim(),
|
||||
share: (Number(royaltyDetails.share) / 100).toString(),
|
||||
}
|
||||
}
|
||||
|
||||
const msg = {
|
||||
create_minter: {
|
||||
init_msg: {
|
||||
nft_data: {
|
||||
nft_data_type: metadataStorageMethod === 'off-chain' ? 'off_chain_metadata' : 'on_chain_metadata',
|
||||
token_uri: metadataStorageMethod === 'off-chain' ? uri : null,
|
||||
extension:
|
||||
metadataStorageMethod === 'on-chain'
|
||||
? {
|
||||
image: uri,
|
||||
name: onChainMetadataInputDetails?.name,
|
||||
description: onChainMetadataInputDetails?.description,
|
||||
attributes: onChainMetadataInputDetails?.attributes,
|
||||
external_url: onChainMetadataInputDetails?.external_url,
|
||||
animation_url:
|
||||
imageUploadDetails?.uploadMethod === 'existing'
|
||||
? onChainMetadataInputDetails?.animation_url
|
||||
: getAssetType(imageUploadDetails?.assetFile?.name as string) === 'video'
|
||||
? uri
|
||||
: undefined,
|
||||
youtube_url: onChainMetadataInputDetails?.youtube_url,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
start_time: mintingDetails?.startTime,
|
||||
end_time: mintingDetails?.endTime,
|
||||
mint_price: {
|
||||
amount: (Number(mintingDetails?.unitPrice) * 1000000).toString(),
|
||||
denom: 'ustars',
|
||||
},
|
||||
per_address_limit: mintingDetails?.perAddressLimit,
|
||||
payment_address: mintingDetails?.paymentAddress || null,
|
||||
},
|
||||
collection_params: {
|
||||
code_id: collectionDetails?.updatable ? SG721_UPDATABLE_CODE_ID : SG721_CODE_ID,
|
||||
name: collectionDetails?.name,
|
||||
symbol: collectionDetails?.symbol,
|
||||
info: {
|
||||
creator: wallet.address,
|
||||
description: collectionDetails?.description,
|
||||
image: coverImageUri,
|
||||
explicit_content: collectionDetails?.explicit || false,
|
||||
royalty_info: royaltyInfo,
|
||||
start_trading_time: collectionDetails?.startTradingTime || null,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
console.log('msg: ', msg)
|
||||
|
||||
const payload: OpenEditionFactoryDispatchExecuteArgs = {
|
||||
contract: collectionDetails?.updatable ? OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS : OPEN_EDITION_FACTORY_ADDRESS,
|
||||
messages: openEditionFactoryMessages,
|
||||
txSigner: wallet.address,
|
||||
msg,
|
||||
funds: [
|
||||
coin(
|
||||
collectionDetails?.updatable
|
||||
? (openEditionMinterUpdatableCreationFee as string)
|
||||
: (openEditionMinterCreationFee as string),
|
||||
'ustars',
|
||||
),
|
||||
],
|
||||
updatable: collectionDetails?.updatable,
|
||||
}
|
||||
await openEditionFactoryDispatchExecute(payload)
|
||||
.then((data) => {
|
||||
setTransactionHash(data.transactionHash)
|
||||
setOpenEditionMinterContractAddress(data.openEditionMinterAddress)
|
||||
setSg721ContractAddress(data.sg721Address)
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
setUploading(false)
|
||||
setCreationInProgress(false)
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
if (minterType !== 'openEdition') {
|
||||
setTransactionHash(null)
|
||||
setOpenEditionMinterContractAddress(null)
|
||||
setSg721ContractAddress(null)
|
||||
setCreationInProgress(false)
|
||||
setUploading(false)
|
||||
}
|
||||
}, [minterType])
|
||||
|
||||
useEffect(() => {
|
||||
const data: OpenEditionMinterCreatorDataProps = {
|
||||
metadataStorageMethod,
|
||||
openEditionMinterContractAddress,
|
||||
sg721ContractAddress,
|
||||
transactionHash,
|
||||
}
|
||||
onChange(data)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [metadataStorageMethod, openEditionMinterContractAddress, sg721ContractAddress, transactionHash])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mx-10 mb-4 rounded border-2 border-white/20">
|
||||
<div className="flex justify-center mb-2">
|
||||
<div className="mt-3 ml-4 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={metadataStorageMethod === 'off-chain'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio9"
|
||||
name="inlineRadioOptions9"
|
||||
onClick={() => {
|
||||
setMetadataStorageMethod('off-chain')
|
||||
}}
|
||||
type="radio"
|
||||
value="Off Chain"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio9"
|
||||
>
|
||||
Off-Chain Metadata
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-3 ml-2 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={metadataStorageMethod === 'on-chain'}
|
||||
className="peer sr-only"
|
||||
id="inlineRadio10"
|
||||
name="inlineRadioOptions10"
|
||||
onClick={() => {
|
||||
setMetadataStorageMethod('on-chain')
|
||||
}}
|
||||
type="radio"
|
||||
value="On Chain"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="inlineRadio10"
|
||||
>
|
||||
On-Chain Metadata
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx('my-4 mx-10')}>
|
||||
<Conditional test={metadataStorageMethod === 'off-chain'}>
|
||||
<div>
|
||||
<OffChainMetadataUploadDetails onChange={setOffChainMetadataUploadDetails} />
|
||||
</div>
|
||||
</Conditional>
|
||||
<Conditional test={metadataStorageMethod === 'on-chain'}>
|
||||
<div>
|
||||
<ImageUploadDetails onChange={setImageUploadDetails} />
|
||||
<OnChainMetadataInputDetails
|
||||
onChange={setOnChainMetadataInputDetails}
|
||||
uploadMethod={imageUploadDetails?.uploadMethod}
|
||||
/>
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
<div className="flex justify-between py-3 px-8 mx-10 rounded border-2 border-white/20 grid-col-2">
|
||||
<CollectionDetails
|
||||
coverImageUrl={
|
||||
metadataStorageMethod === 'off-chain'
|
||||
? (offChainMetadataUploadDetails?.imageUrl as string)
|
||||
: (imageUploadDetails?.coverImageUrl as string)
|
||||
}
|
||||
metadataStorageMethod={metadataStorageMethod}
|
||||
onChange={setCollectionDetails}
|
||||
uploadMethod={
|
||||
metadataStorageMethod === 'off-chain'
|
||||
? (offChainMetadataUploadDetails?.uploadMethod as UploadMethod)
|
||||
: (imageUploadDetails?.uploadMethod as UploadMethod)
|
||||
}
|
||||
/>
|
||||
<MintingDetails
|
||||
minimumMintPrice={
|
||||
collectionDetails?.updatable
|
||||
? Number(minimumUpdatableMintPrice) / 1000000
|
||||
: Number(minimumMintPrice) / 1000000
|
||||
}
|
||||
onChange={setMintingDetails}
|
||||
uploadMethod={offChainMetadataUploadDetails?.uploadMethod as UploadMethod}
|
||||
/>
|
||||
</div>
|
||||
<div className="my-6">
|
||||
<RoyaltyDetails onChange={setRoyaltyDetails} />
|
||||
</div>
|
||||
<div className="flex justify-end w-full">
|
||||
<Button
|
||||
className="relative justify-center p-2 mr-12 mb-6 max-h-12 text-white bg-plumbus hover:bg-plumbus-light border-0"
|
||||
isLoading={creationInProgress}
|
||||
onClick={performOpenEditionMinterChecks}
|
||||
variant="solid"
|
||||
>
|
||||
Create Collection
|
||||
</Button>
|
||||
</div>
|
||||
<Conditional test={uploading}>
|
||||
<LoadingModal />
|
||||
</Conditional>
|
||||
<Conditional test={readyToCreate}>
|
||||
<ConfirmationModal confirm={createOpenEditionMinter} />
|
||||
</Conditional>
|
||||
</div>
|
||||
)
|
||||
}
|
113
components/openEdition/RoyaltyDetails.tsx
Normal file
113
components/openEdition/RoyaltyDetails.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { FormGroup } from 'components/FormGroup'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
|
||||
import { NumberInput, TextInput } from '../forms/FormInput'
|
||||
|
||||
interface RoyaltyDetailsProps {
|
||||
onChange: (data: RoyaltyDetailsDataProps) => void
|
||||
}
|
||||
|
||||
export interface RoyaltyDetailsDataProps {
|
||||
royaltyType: RoyaltyState
|
||||
paymentAddress: string
|
||||
share: number
|
||||
}
|
||||
|
||||
type RoyaltyState = 'none' | 'new'
|
||||
|
||||
export const RoyaltyDetails = ({ onChange }: RoyaltyDetailsProps) => {
|
||||
const wallet = useWallet()
|
||||
const [royaltyState, setRoyaltyState] = useState<RoyaltyState>('none')
|
||||
|
||||
const royaltyPaymentAddressState = useInputState({
|
||||
id: 'royalty-payment-address',
|
||||
name: 'royaltyPaymentAddress',
|
||||
title: 'Payment Address',
|
||||
subtitle: 'Address to receive royalties',
|
||||
placeholder: 'stars1234567890abcdefghijklmnopqrstuvwxyz...',
|
||||
})
|
||||
|
||||
const royaltyShareState = useInputState({
|
||||
id: 'royalty-share',
|
||||
name: 'royaltyShare',
|
||||
title: 'Share Percentage',
|
||||
subtitle: 'Percentage of royalties to be paid',
|
||||
placeholder: '5%',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
void resolveAddress(
|
||||
royaltyPaymentAddressState.value
|
||||
.toLowerCase()
|
||||
.replace(/,/g, '')
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '')
|
||||
.replace(/ /g, ''),
|
||||
wallet,
|
||||
).then((royaltyPaymentAddress) => {
|
||||
royaltyPaymentAddressState.onChange(royaltyPaymentAddress)
|
||||
const data: RoyaltyDetailsDataProps = {
|
||||
royaltyType: royaltyState,
|
||||
paymentAddress: royaltyPaymentAddressState.value,
|
||||
share: Number(royaltyShareState.value),
|
||||
}
|
||||
onChange(data)
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [royaltyState, royaltyPaymentAddressState.value, royaltyShareState.value])
|
||||
|
||||
return (
|
||||
<div className="py-3 px-8 mx-10 rounded border-2 border-white/20">
|
||||
<div className="flex justify-center">
|
||||
<div className="ml-4 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={royaltyState === 'none'}
|
||||
className="peer sr-only"
|
||||
id="royaltyRadio1"
|
||||
name="royaltyRadioOptions1"
|
||||
onClick={() => {
|
||||
setRoyaltyState('none')
|
||||
}}
|
||||
type="radio"
|
||||
value="None"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="royaltyRadio1"
|
||||
>
|
||||
No royalty
|
||||
</label>
|
||||
</div>
|
||||
<div className="ml-4 font-bold form-check form-check-inline">
|
||||
<input
|
||||
checked={royaltyState === 'new'}
|
||||
className="peer sr-only"
|
||||
id="royaltyRadio2"
|
||||
name="royaltyRadioOptions2"
|
||||
onClick={() => {
|
||||
setRoyaltyState('new')
|
||||
}}
|
||||
type="radio"
|
||||
value="Existing"
|
||||
/>
|
||||
<label
|
||||
className="inline-block py-1 px-2 text-gray peer-checked:text-white hover:text-white peer-checked:bg-black hover:rounded-sm peer-checked:border-b-2 hover:border-b-2 peer-checked:border-plumbus hover:border-plumbus cursor-pointer form-check-label"
|
||||
htmlFor="royaltyRadio2"
|
||||
>
|
||||
Configure royalty details
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<Conditional test={royaltyState === 'new'}>
|
||||
<FormGroup subtitle="Information about royalty" title="Royalty Details">
|
||||
<TextInput {...royaltyPaymentAddressState} isRequired />
|
||||
<NumberInput {...royaltyShareState} isRequired />
|
||||
</FormGroup>
|
||||
</Conditional>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -4,6 +4,8 @@ import type { UseBaseFactoryContractProps } from 'contracts/baseFactory'
|
||||
import { useBaseFactoryContract } from 'contracts/baseFactory'
|
||||
import type { UseBaseMinterContractProps } from 'contracts/baseMinter'
|
||||
import { useBaseMinterContract } from 'contracts/baseMinter'
|
||||
import { type UseOpenEditionFactoryContractProps, useOpenEditionFactoryContract } from 'contracts/openEditionFactory'
|
||||
import { type UseOpenEditionMinterContractProps, useOpenEditionMinterContract } from 'contracts/openEditionMinter'
|
||||
import type { UseSG721ContractProps } from 'contracts/sg721'
|
||||
import { useSG721Contract } from 'contracts/sg721'
|
||||
import type { UseVendingFactoryContractProps } from 'contracts/vendingFactory'
|
||||
@ -27,9 +29,11 @@ export interface ContractsStore extends State {
|
||||
sg721: UseSG721ContractProps | null
|
||||
vendingMinter: UseVendingMinterContractProps | null
|
||||
baseMinter: UseBaseMinterContractProps | null
|
||||
openEditionMinter: UseOpenEditionMinterContractProps | null
|
||||
whitelist: UseWhiteListContractProps | null
|
||||
vendingFactory: UseVendingFactoryContractProps | null
|
||||
baseFactory: UseBaseFactoryContractProps | null
|
||||
openEditionFactory: UseOpenEditionFactoryContractProps | null
|
||||
badgeHub: UseBadgeHubContractProps | null
|
||||
splits: UseSplitsContractProps | null
|
||||
}
|
||||
@ -41,9 +45,11 @@ export const defaultValues: ContractsStore = {
|
||||
sg721: null,
|
||||
vendingMinter: null,
|
||||
baseMinter: null,
|
||||
openEditionMinter: null,
|
||||
whitelist: null,
|
||||
vendingFactory: null,
|
||||
baseFactory: null,
|
||||
openEditionFactory: null,
|
||||
badgeHub: null,
|
||||
splits: null,
|
||||
}
|
||||
@ -72,9 +78,11 @@ const ContractsSubscription: VFC = () => {
|
||||
const sg721 = useSG721Contract()
|
||||
const vendingMinter = useVendingMinterContract()
|
||||
const baseMinter = useBaseMinterContract()
|
||||
const openEditionMinter = useOpenEditionMinterContract()
|
||||
const whitelist = useWhiteListContract()
|
||||
const vendingFactory = useVendingFactoryContract()
|
||||
const baseFactory = useBaseFactoryContract()
|
||||
const openEditionFactory = useOpenEditionFactoryContract()
|
||||
const badgeHub = useBadgeHubContract()
|
||||
const splits = useSplitsContract()
|
||||
|
||||
@ -83,9 +91,11 @@ const ContractsSubscription: VFC = () => {
|
||||
sg721,
|
||||
vendingMinter,
|
||||
baseMinter,
|
||||
openEditionMinter,
|
||||
whitelist,
|
||||
vendingFactory,
|
||||
baseFactory,
|
||||
openEditionFactory,
|
||||
badgeHub,
|
||||
splits,
|
||||
})
|
||||
|
104
contracts/openEditionFactory/contract.ts
Normal file
104
contracts/openEditionFactory/contract.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
|
||||
import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||
import type { Coin } from '@cosmjs/proto-signing'
|
||||
import type { logs } from '@cosmjs/stargate'
|
||||
import { OPEN_EDITION_FACTORY_ADDRESS, OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS } from 'utils/constants'
|
||||
|
||||
export interface CreateOpenEditionMinterResponse {
|
||||
readonly openEditionMinterAddress: string
|
||||
readonly sg721Address: string
|
||||
readonly transactionHash: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface OpenEditionFactoryInstance {
|
||||
readonly contractAddress: string
|
||||
|
||||
//Query
|
||||
|
||||
//Execute
|
||||
createOpenEditionMinter: (
|
||||
senderAddress: string,
|
||||
msg: Record<string, unknown>,
|
||||
funds: Coin[],
|
||||
updatable?: boolean,
|
||||
) => Promise<CreateOpenEditionMinterResponse>
|
||||
}
|
||||
|
||||
export interface OpenEditionFactoryMessages {
|
||||
createOpenEditionMinter: (
|
||||
msg: Record<string, unknown>,
|
||||
funds: Coin[],
|
||||
updatable?: boolean,
|
||||
) => CreateOpenEditionMinterMessage
|
||||
}
|
||||
|
||||
export interface CreateOpenEditionMinterMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: Record<string, unknown>
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface OpenEditionFactoryContract {
|
||||
use: (contractAddress: string) => OpenEditionFactoryInstance
|
||||
|
||||
messages: (contractAddress: string) => OpenEditionFactoryMessages
|
||||
}
|
||||
|
||||
export const openEditionFactory = (client: SigningCosmWasmClient, txSigner: string): OpenEditionFactoryContract => {
|
||||
const use = (contractAddress: string): OpenEditionFactoryInstance => {
|
||||
//Query
|
||||
|
||||
//Execute
|
||||
const createOpenEditionMinter = async (
|
||||
senderAddress: string,
|
||||
msg: Record<string, unknown>,
|
||||
funds: Coin[],
|
||||
updatable?: boolean,
|
||||
): Promise<CreateOpenEditionMinterResponse> => {
|
||||
const result = await client.execute(
|
||||
senderAddress,
|
||||
updatable ? OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS : OPEN_EDITION_FACTORY_ADDRESS,
|
||||
msg,
|
||||
'auto',
|
||||
'',
|
||||
funds,
|
||||
)
|
||||
|
||||
return {
|
||||
openEditionMinterAddress: result.logs[0].events[5].attributes[0].value,
|
||||
sg721Address: result.logs[0].events[5].attributes[2].value,
|
||||
transactionHash: result.transactionHash,
|
||||
logs: result.logs,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
contractAddress,
|
||||
createOpenEditionMinter,
|
||||
}
|
||||
}
|
||||
|
||||
const messages = (contractAddress: string) => {
|
||||
const createOpenEditionMinter = (
|
||||
msg: Record<string, unknown>,
|
||||
funds: Coin[],
|
||||
updatable?: boolean,
|
||||
): CreateOpenEditionMinterMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg,
|
||||
funds,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createOpenEditionMinter,
|
||||
}
|
||||
}
|
||||
|
||||
return { use, messages }
|
||||
}
|
2
contracts/openEditionFactory/index.ts
Normal file
2
contracts/openEditionFactory/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './contract'
|
||||
export * from './useContract'
|
31
contracts/openEditionFactory/messages/execute.ts
Normal file
31
contracts/openEditionFactory/messages/execute.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||
|
||||
import type { Coin } from '@cosmjs/proto-signing'
|
||||
|
||||
import type { OpenEditionFactoryInstance } from '../index'
|
||||
import { useOpenEditionFactoryContract } from '../index'
|
||||
|
||||
/** @see {@link OpenEditionFactoryInstance} */
|
||||
export interface DispatchExecuteArgs {
|
||||
contract: string
|
||||
messages?: OpenEditionFactoryInstance
|
||||
txSigner: string
|
||||
msg: Record<string, unknown>
|
||||
funds: Coin[]
|
||||
updatable?: boolean
|
||||
}
|
||||
|
||||
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
const { messages, txSigner } = args
|
||||
if (!messages) {
|
||||
throw new Error('cannot dispatch execute, messages is not defined')
|
||||
}
|
||||
return messages.createOpenEditionMinter(txSigner, args.msg, args.funds, args.updatable)
|
||||
}
|
||||
|
||||
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages } = useOpenEditionFactoryContract()
|
||||
const { contract } = args
|
||||
return messages(contract)?.createOpenEditionMinter(args.msg, args.funds, args.updatable)
|
||||
}
|
76
contracts/openEditionFactory/useContract.ts
Normal file
76
contracts/openEditionFactory/useContract.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import type { logs } from '@cosmjs/stargate'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import type { OpenEditionFactoryContract, OpenEditionFactoryInstance, OpenEditionFactoryMessages } from './contract'
|
||||
import { openEditionFactory as initContract } from './contract'
|
||||
|
||||
/*export interface InstantiateResponse {
|
||||
/** The address of the newly instantiated contract *-/
|
||||
readonly contractAddress: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
/** Block height in which the transaction is included *-/
|
||||
readonly height: number
|
||||
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex *-/
|
||||
readonly transactionHash: string
|
||||
readonly gasWanted: number
|
||||
readonly gasUsed: number
|
||||
}*/
|
||||
|
||||
interface InstantiateResponse {
|
||||
readonly contractAddress: string
|
||||
readonly transactionHash: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface UseOpenEditionFactoryContractProps {
|
||||
use: (customAddress: string) => OpenEditionFactoryInstance | undefined
|
||||
updateContractAddress: (contractAddress: string) => void
|
||||
getContractAddress: () => string | undefined
|
||||
messages: (contractAddress: string) => OpenEditionFactoryMessages | undefined
|
||||
}
|
||||
|
||||
export function useOpenEditionFactoryContract(): UseOpenEditionFactoryContractProps {
|
||||
const wallet = useWallet()
|
||||
|
||||
const [address, setAddress] = useState<string>('')
|
||||
const [openEditionFactory, setOpenEditionFactory] = useState<OpenEditionFactoryContract>()
|
||||
|
||||
useEffect(() => {
|
||||
setAddress(localStorage.getItem('contract_address') || '')
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const OpenEditionFactoryBaseContract = initContract(wallet.getClient(), wallet.address)
|
||||
setOpenEditionFactory(OpenEditionFactoryBaseContract)
|
||||
}, [wallet])
|
||||
|
||||
const updateContractAddress = (contractAddress: string) => {
|
||||
setAddress(contractAddress)
|
||||
}
|
||||
|
||||
const use = useCallback(
|
||||
(customAddress = ''): OpenEditionFactoryInstance | undefined => {
|
||||
return openEditionFactory?.use(address || customAddress)
|
||||
},
|
||||
[openEditionFactory, address],
|
||||
)
|
||||
|
||||
const getContractAddress = (): string | undefined => {
|
||||
return address
|
||||
}
|
||||
|
||||
const messages = useCallback(
|
||||
(customAddress = ''): OpenEditionFactoryMessages | undefined => {
|
||||
return openEditionFactory?.messages(address || customAddress)
|
||||
},
|
||||
[openEditionFactory, address],
|
||||
)
|
||||
|
||||
return {
|
||||
use,
|
||||
updateContractAddress,
|
||||
getContractAddress,
|
||||
messages,
|
||||
}
|
||||
}
|
588
contracts/openEditionMinter/contract.ts
Normal file
588
contracts/openEditionMinter/contract.ts
Normal file
@ -0,0 +1,588 @@
|
||||
import type { MsgExecuteContractEncodeObject, SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate'
|
||||
import { toUtf8 } from '@cosmjs/encoding'
|
||||
import type { Coin } from '@cosmjs/proto-signing'
|
||||
import { coin } from '@cosmjs/proto-signing'
|
||||
import type { logs } from '@cosmjs/stargate'
|
||||
import type { Timestamp } from '@stargazezone/types/contracts/minter/shared-types'
|
||||
import { MsgExecuteContract } from 'cosmjs-types/cosmwasm/wasm/v1/tx'
|
||||
|
||||
export interface InstantiateResponse {
|
||||
readonly contractAddress: string
|
||||
readonly transactionHash: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface MigrateResponse {
|
||||
readonly transactionHash: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface RoyaltyInfo {
|
||||
payment_address: string
|
||||
share: string
|
||||
}
|
||||
|
||||
export interface OpenEditionMinterInstance {
|
||||
readonly contractAddress: string
|
||||
|
||||
//Query
|
||||
getConfig: () => Promise<any>
|
||||
getStartTime: () => Promise<any>
|
||||
getEndTime: () => Promise<any>
|
||||
getMintPrice: () => Promise<any>
|
||||
getMintCount: (address: string) => Promise<any>
|
||||
getTotalMintCount: () => Promise<any>
|
||||
getStatus: () => Promise<any>
|
||||
|
||||
//Execute
|
||||
mint: (senderAddress: string) => Promise<string>
|
||||
purge: (senderAddress: string) => Promise<string>
|
||||
updateMintPrice: (senderAddress: string, price: string) => Promise<string>
|
||||
updateStartTime: (senderAddress: string, time: Timestamp) => Promise<string>
|
||||
updateEndTime: (senderAddress: string, time: Timestamp) => Promise<string>
|
||||
updateStartTradingTime: (senderAddress: string, time?: Timestamp) => Promise<string>
|
||||
updatePerAddressLimit: (senderAddress: string, perAddressLimit: number) => Promise<string>
|
||||
mintTo: (senderAddress: string, recipient: string) => Promise<string>
|
||||
batchMint: (senderAddress: string, recipient: string, batchNumber: number) => Promise<string>
|
||||
airdrop: (senderAddress: string, recipients: string[]) => Promise<string>
|
||||
}
|
||||
|
||||
export interface OpenEditionMinterMessages {
|
||||
mint: () => MintMessage
|
||||
purge: () => PurgeMessage
|
||||
updateMintPrice: (price: string) => UpdateMintPriceMessage
|
||||
updateStartTime: (time: Timestamp) => UpdateStartTimeMessage
|
||||
updateEndTime: (time: Timestamp) => UpdateEndTimeMessage
|
||||
updateStartTradingTime: (time: Timestamp) => UpdateStartTradingTimeMessage
|
||||
updatePerAddressLimit: (perAddressLimit: number) => UpdatePerAddressLimitMessage
|
||||
mintTo: (recipient: string) => MintToMessage
|
||||
batchMint: (recipient: string, batchNumber: number) => CustomMessage
|
||||
airdrop: (recipients: string[]) => CustomMessage
|
||||
}
|
||||
|
||||
export interface MintMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
mint: Record<string, never>
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface PurgeMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
purge: Record<string, never>
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdateStartTradingTimeMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
update_start_trading_time: string
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdateStartTimeMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
update_start_time: string
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdateEndTimeMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
update_end_time: string
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdateMintPriceMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
update_mint_price: {
|
||||
price: string
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface UpdatePerAddressLimitMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
update_per_address_limit: {
|
||||
per_address_limit: number
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface MintToMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: {
|
||||
mint_to: {
|
||||
recipient: string
|
||||
}
|
||||
}
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface CustomMessage {
|
||||
sender: string
|
||||
contract: string
|
||||
msg: Record<string, unknown>[]
|
||||
funds: Coin[]
|
||||
}
|
||||
|
||||
export interface MintPriceMessage {
|
||||
public_price: {
|
||||
denom: string
|
||||
amount: string
|
||||
}
|
||||
airdrop_price: {
|
||||
denom: string
|
||||
amount: string
|
||||
}
|
||||
current_price: {
|
||||
denom: string
|
||||
amount: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface OpenEditionMinterContract {
|
||||
instantiate: (
|
||||
senderAddress: string,
|
||||
codeId: number,
|
||||
initMsg: Record<string, unknown>,
|
||||
label: string,
|
||||
admin?: string,
|
||||
funds?: Coin[],
|
||||
) => Promise<InstantiateResponse>
|
||||
|
||||
migrate: (
|
||||
senderAddress: string,
|
||||
contractAddress: string,
|
||||
codeId: number,
|
||||
migrateMsg: Record<string, unknown>,
|
||||
) => Promise<MigrateResponse>
|
||||
|
||||
use: (contractAddress: string) => OpenEditionMinterInstance
|
||||
|
||||
messages: (contractAddress: string) => OpenEditionMinterMessages
|
||||
}
|
||||
|
||||
export const openEditionMinter = (client: SigningCosmWasmClient, txSigner: string): OpenEditionMinterContract => {
|
||||
const use = (contractAddress: string): OpenEditionMinterInstance => {
|
||||
//Query
|
||||
const getConfig = async (): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
config: {},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getStartTime = async (): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
start_time: {},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getEndTime = async (): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
end_time: {},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getMintPrice = async (): Promise<MintPriceMessage> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
mint_price: {},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getMintCount = async (address: string): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
mint_count: { address },
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getTotalMintCount = async (): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
total_mint_count: {},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const getStatus = async (): Promise<any> => {
|
||||
const res = await client.queryContractSmart(contractAddress, {
|
||||
status: {},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
//Execute
|
||||
const mint = async (senderAddress: string): Promise<string> => {
|
||||
const price = (await getMintPrice()).public_price.amount
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
mint: {},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
[coin(price, 'ustars')],
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const purge = async (senderAddress: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
purge: {},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const updateStartTradingTime = async (senderAddress: string, time?: Timestamp): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
update_start_trading_time: time || null,
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const updateStartTime = async (senderAddress: string, time: Timestamp): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
update_start_time: time,
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const updateEndTime = async (senderAddress: string, time: Timestamp): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
update_end_time: time,
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const updateMintPrice = async (senderAddress: string, price: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
update_mint_price: {
|
||||
price: (Number(price) * 1000000).toString(),
|
||||
},
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const updatePerAddressLimit = async (senderAddress: string, perAddressLimit: number): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
update_per_address_limit: { per_address_limit: perAddressLimit },
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const mintTo = async (senderAddress: string, recipient: string): Promise<string> => {
|
||||
const res = await client.execute(
|
||||
senderAddress,
|
||||
contractAddress,
|
||||
{
|
||||
mint_to: { recipient },
|
||||
},
|
||||
'auto',
|
||||
'',
|
||||
)
|
||||
|
||||
return res.transactionHash
|
||||
}
|
||||
|
||||
const batchMint = async (senderAddress: string, recipient: string, batchNumber: number): Promise<string> => {
|
||||
const executeContractMsgs: MsgExecuteContractEncodeObject[] = []
|
||||
for (let i = 0; i < batchNumber; i++) {
|
||||
const msg = {
|
||||
mint_to: { recipient },
|
||||
}
|
||||
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', 'batch mint')
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return {
|
||||
contractAddress,
|
||||
getConfig,
|
||||
getStartTime,
|
||||
getEndTime,
|
||||
getMintPrice,
|
||||
getMintCount,
|
||||
getTotalMintCount,
|
||||
getStatus,
|
||||
mint,
|
||||
purge,
|
||||
updateStartTradingTime,
|
||||
updateStartTime,
|
||||
updateEndTime,
|
||||
updateMintPrice,
|
||||
updatePerAddressLimit,
|
||||
mintTo,
|
||||
batchMint,
|
||||
airdrop,
|
||||
}
|
||||
}
|
||||
|
||||
const migrate = async (
|
||||
senderAddress: string,
|
||||
contractAddress: string,
|
||||
codeId: number,
|
||||
migrateMsg: Record<string, unknown>,
|
||||
): Promise<MigrateResponse> => {
|
||||
const result = await client.migrate(senderAddress, contractAddress, codeId, migrateMsg, 'auto')
|
||||
return {
|
||||
transactionHash: result.transactionHash,
|
||||
logs: result.logs,
|
||||
}
|
||||
}
|
||||
|
||||
const instantiate = async (
|
||||
senderAddress: string,
|
||||
codeId: number,
|
||||
initMsg: Record<string, unknown>,
|
||||
label: string,
|
||||
): Promise<InstantiateResponse> => {
|
||||
const result = await client.instantiate(senderAddress, codeId, initMsg, label, 'auto', {
|
||||
funds: [coin('1000000000', 'ustars')],
|
||||
})
|
||||
|
||||
return {
|
||||
contractAddress: result.contractAddress,
|
||||
transactionHash: result.transactionHash,
|
||||
logs: result.logs,
|
||||
}
|
||||
}
|
||||
|
||||
const messages = (contractAddress: string) => {
|
||||
const mint = (): MintMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
mint: {},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const purge = (): PurgeMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
purge: {},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const updateStartTradingTime = (startTime: string): UpdateStartTradingTimeMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
update_start_trading_time: startTime,
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const updateStartTime = (startTime: string): UpdateStartTimeMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
update_start_time: startTime,
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const updateEndTime = (endTime: string): UpdateEndTimeMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
update_end_time: endTime,
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const updateMintPrice = (price: string): UpdateMintPriceMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
update_mint_price: {
|
||||
price: (Number(price) * 1000000).toString(),
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const updatePerAddressLimit = (limit: number): UpdatePerAddressLimitMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
update_per_address_limit: {
|
||||
per_address_limit: limit,
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const mintTo = (recipient: string): MintToMessage => {
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg: {
|
||||
mint_to: {
|
||||
recipient,
|
||||
},
|
||||
},
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
const batchMint = (recipient: string, batchNumber: number): CustomMessage => {
|
||||
const msg: Record<string, unknown>[] = []
|
||||
for (let i = 0; i < batchNumber; i++) {
|
||||
msg.push({ mint_to: { recipient } })
|
||||
}
|
||||
return {
|
||||
sender: txSigner,
|
||||
contract: contractAddress,
|
||||
msg,
|
||||
funds: [],
|
||||
}
|
||||
}
|
||||
|
||||
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: [],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mint,
|
||||
purge,
|
||||
updateStartTradingTime,
|
||||
updateStartTime,
|
||||
updateEndTime,
|
||||
updateMintPrice,
|
||||
updatePerAddressLimit,
|
||||
mintTo,
|
||||
batchMint,
|
||||
airdrop,
|
||||
}
|
||||
}
|
||||
|
||||
return { use, instantiate, migrate, messages }
|
||||
}
|
2
contracts/openEditionMinter/index.ts
Normal file
2
contracts/openEditionMinter/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './contract'
|
||||
export * from './useContract'
|
163
contracts/openEditionMinter/messages/execute.ts
Normal file
163
contracts/openEditionMinter/messages/execute.ts
Normal file
@ -0,0 +1,163 @@
|
||||
import type { OpenEditionMinterInstance } from '../index'
|
||||
import { useOpenEditionMinterContract } from '../index'
|
||||
|
||||
export type ExecuteType = typeof EXECUTE_TYPES[number]
|
||||
|
||||
export const EXECUTE_TYPES = [
|
||||
'mint',
|
||||
'update_start_time',
|
||||
'update_end_time',
|
||||
'update_mint_price',
|
||||
'update_start_trading_time',
|
||||
'update_per_address_limit',
|
||||
'mint_to',
|
||||
'purge',
|
||||
] as const
|
||||
|
||||
export interface ExecuteListItem {
|
||||
id: ExecuteType
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const EXECUTE_LIST: ExecuteListItem[] = [
|
||||
{
|
||||
id: 'mint',
|
||||
name: 'Mint',
|
||||
description: `Mint a new token`,
|
||||
},
|
||||
{
|
||||
id: 'update_mint_price',
|
||||
name: 'Update Mint Price',
|
||||
description: `Update the mint price per token`,
|
||||
},
|
||||
{
|
||||
id: 'update_start_time',
|
||||
name: 'Update Start Time',
|
||||
description: `Update the start time for minting`,
|
||||
},
|
||||
{
|
||||
id: 'update_end_time',
|
||||
name: 'Update End Time',
|
||||
description: `Update the end time for minting`,
|
||||
},
|
||||
{
|
||||
id: 'update_start_trading_time',
|
||||
name: 'Update Start Trading Time',
|
||||
description: `Update start trading time for minting`,
|
||||
},
|
||||
{
|
||||
id: 'update_per_address_limit',
|
||||
name: 'Update Per Address Limit',
|
||||
description: `Update token per address limit`,
|
||||
},
|
||||
{
|
||||
id: 'mint_to',
|
||||
name: 'Mint To',
|
||||
description: `Mint tokens to a given address`,
|
||||
},
|
||||
{
|
||||
id: 'purge',
|
||||
name: 'Purge',
|
||||
description: `Purge`,
|
||||
},
|
||||
]
|
||||
|
||||
export interface DispatchExecuteProps {
|
||||
type: ExecuteType
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
type Select<T extends ExecuteType> = T
|
||||
|
||||
/** @see {@link OpenEditionMinterInstance} */
|
||||
export type DispatchExecuteArgs = {
|
||||
contract: string
|
||||
messages?: OpenEditionMinterInstance
|
||||
txSigner: string
|
||||
} & (
|
||||
| { type: undefined }
|
||||
| { type: Select<'mint'> }
|
||||
| { type: Select<'purge'> }
|
||||
| { type: Select<'update_start_time'>; startTime: string }
|
||||
| { type: Select<'update_end_time'>; endTime: string }
|
||||
| { type: Select<'update_mint_price'>; price: string }
|
||||
| { type: Select<'update_start_trading_time'>; startTime?: string }
|
||||
| { type: Select<'update_per_address_limit'>; limit: number }
|
||||
| { type: Select<'mint_to'>; recipient: string }
|
||||
)
|
||||
|
||||
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
const { messages, txSigner } = args
|
||||
if (!messages) {
|
||||
throw new Error('cannot dispatch execute, messages is not defined')
|
||||
}
|
||||
switch (args.type) {
|
||||
case 'mint': {
|
||||
return messages.mint(txSigner)
|
||||
}
|
||||
case 'purge': {
|
||||
return messages.purge(txSigner)
|
||||
}
|
||||
case 'update_start_time': {
|
||||
return messages.updateStartTime(txSigner, args.startTime)
|
||||
}
|
||||
case 'update_end_time': {
|
||||
return messages.updateEndTime(txSigner, args.endTime)
|
||||
}
|
||||
case 'update_mint_price': {
|
||||
return messages.updateMintPrice(txSigner, args.price)
|
||||
}
|
||||
case 'update_start_trading_time': {
|
||||
return messages.updateStartTradingTime(txSigner, args.startTime)
|
||||
}
|
||||
case 'update_per_address_limit': {
|
||||
return messages.updatePerAddressLimit(txSigner, args.limit)
|
||||
}
|
||||
case 'mint_to': {
|
||||
return messages.mintTo(txSigner, args.recipient)
|
||||
}
|
||||
default: {
|
||||
throw new Error('unknown execute type')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages } = useOpenEditionMinterContract()
|
||||
const { contract } = args
|
||||
switch (args.type) {
|
||||
case 'mint': {
|
||||
return messages(contract)?.mint()
|
||||
}
|
||||
case 'purge': {
|
||||
return messages(contract)?.purge()
|
||||
}
|
||||
case 'update_start_time': {
|
||||
return messages(contract)?.updateStartTime(args.startTime)
|
||||
}
|
||||
case 'update_end_time': {
|
||||
return messages(contract)?.updateEndTime(args.endTime)
|
||||
}
|
||||
case 'update_mint_price': {
|
||||
return messages(contract)?.updateMintPrice(args.price)
|
||||
}
|
||||
case 'update_start_trading_time': {
|
||||
return messages(contract)?.updateStartTradingTime(args.startTime as string)
|
||||
}
|
||||
case 'update_per_address_limit': {
|
||||
return messages(contract)?.updatePerAddressLimit(args.limit)
|
||||
}
|
||||
case 'mint_to': {
|
||||
return messages(contract)?.mintTo(args.recipient)
|
||||
}
|
||||
default: {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const isEitherType = <T extends ExecuteType>(type: unknown, arr: T[]): type is T => {
|
||||
return arr.some((val) => type === val)
|
||||
}
|
73
contracts/openEditionMinter/messages/query.ts
Normal file
73
contracts/openEditionMinter/messages/query.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import type { OpenEditionMinterInstance } from '../contract'
|
||||
|
||||
export type QueryType = typeof QUERY_TYPES[number]
|
||||
|
||||
export const QUERY_TYPES = [
|
||||
'config',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'mint_price',
|
||||
'mint_count',
|
||||
'total_mint_count',
|
||||
'status',
|
||||
] as const
|
||||
|
||||
export interface QueryListItem {
|
||||
id: QueryType
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export const QUERY_LIST: QueryListItem[] = [
|
||||
{ id: 'config', name: 'Config', description: 'View current config' },
|
||||
{ id: 'start_time', name: 'Start Time', description: 'View the start time for minting' },
|
||||
{ id: 'end_time', name: 'End Time', description: 'View the end time for minting' },
|
||||
{ id: 'mint_price', name: 'Mint Price', description: 'View the mint price' },
|
||||
{
|
||||
id: 'mint_count',
|
||||
name: 'Mint Count for Address',
|
||||
description: 'View the total amount of minted tokens for an address',
|
||||
},
|
||||
{
|
||||
id: 'total_mint_count',
|
||||
name: 'Total Mint Count',
|
||||
description: 'View the total amount of minted tokens',
|
||||
},
|
||||
{ id: 'status', name: 'Status', description: 'View contract status' },
|
||||
]
|
||||
|
||||
export interface DispatchQueryProps {
|
||||
address: string
|
||||
messages: OpenEditionMinterInstance | undefined
|
||||
type: QueryType
|
||||
}
|
||||
|
||||
export const dispatchQuery = (props: DispatchQueryProps) => {
|
||||
const { address, messages, type } = props
|
||||
switch (type) {
|
||||
case 'config': {
|
||||
return messages?.getConfig()
|
||||
}
|
||||
case 'status': {
|
||||
return messages?.getStatus()
|
||||
}
|
||||
case 'start_time': {
|
||||
return messages?.getStartTime()
|
||||
}
|
||||
case 'end_time': {
|
||||
return messages?.getEndTime()
|
||||
}
|
||||
case 'mint_price': {
|
||||
return messages?.getMintPrice()
|
||||
}
|
||||
case 'mint_count': {
|
||||
return messages?.getMintCount(address)
|
||||
}
|
||||
case 'total_mint_count': {
|
||||
return messages?.getTotalMintCount()
|
||||
}
|
||||
default: {
|
||||
throw new Error('unknown query type')
|
||||
}
|
||||
}
|
||||
}
|
119
contracts/openEditionMinter/useContract.ts
Normal file
119
contracts/openEditionMinter/useContract.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import type { Coin } from '@cosmjs/proto-signing'
|
||||
import type { logs } from '@cosmjs/stargate'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import type {
|
||||
MigrateResponse,
|
||||
OpenEditionMinterContract,
|
||||
OpenEditionMinterInstance,
|
||||
OpenEditionMinterMessages,
|
||||
} from './contract'
|
||||
import { openEditionMinter as initContract } from './contract'
|
||||
|
||||
/*export interface InstantiateResponse {
|
||||
/** The address of the newly instantiated contract *-/
|
||||
readonly contractAddress: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
/** Block height in which the transaction is included *-/
|
||||
readonly height: number
|
||||
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex *-/
|
||||
readonly transactionHash: string
|
||||
readonly gasWanted: number
|
||||
readonly gasUsed: number
|
||||
}*/
|
||||
|
||||
interface InstantiateResponse {
|
||||
readonly contractAddress: string
|
||||
readonly transactionHash: string
|
||||
readonly logs: readonly logs.Log[]
|
||||
}
|
||||
|
||||
export interface UseOpenEditionMinterContractProps {
|
||||
instantiate: (
|
||||
codeId: number,
|
||||
initMsg: Record<string, unknown>,
|
||||
label: string,
|
||||
admin?: string,
|
||||
funds?: Coin[],
|
||||
) => Promise<InstantiateResponse>
|
||||
migrate: (contractAddress: string, codeId: number, migrateMsg: Record<string, unknown>) => Promise<MigrateResponse>
|
||||
use: (customAddress: string) => OpenEditionMinterInstance | undefined
|
||||
updateContractAddress: (contractAddress: string) => void
|
||||
getContractAddress: () => string | undefined
|
||||
messages: (contractAddress: string) => OpenEditionMinterMessages | undefined
|
||||
}
|
||||
|
||||
export function useOpenEditionMinterContract(): UseOpenEditionMinterContractProps {
|
||||
const wallet = useWallet()
|
||||
|
||||
const [address, setAddress] = useState<string>('')
|
||||
const [openEditionMinter, setOpenEditionMinter] = useState<OpenEditionMinterContract>()
|
||||
|
||||
useEffect(() => {
|
||||
setAddress(localStorage.getItem('contract_address') || '')
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const OpenEditionMinterBaseContract = initContract(wallet.getClient(), wallet.address)
|
||||
setOpenEditionMinter(OpenEditionMinterBaseContract)
|
||||
}, [wallet])
|
||||
|
||||
const updateContractAddress = (contractAddress: string) => {
|
||||
setAddress(contractAddress)
|
||||
}
|
||||
|
||||
const instantiate = useCallback(
|
||||
(codeId: number, initMsg: Record<string, unknown>, label: string, admin?: string): Promise<InstantiateResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!openEditionMinter) {
|
||||
reject(new Error('Contract is not initialized.'))
|
||||
return
|
||||
}
|
||||
openEditionMinter.instantiate(wallet.address, codeId, initMsg, label, admin).then(resolve).catch(reject)
|
||||
})
|
||||
},
|
||||
[openEditionMinter, wallet],
|
||||
)
|
||||
|
||||
const migrate = useCallback(
|
||||
(contractAddress: string, codeId: number, migrateMsg: Record<string, unknown>): Promise<MigrateResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!openEditionMinter) {
|
||||
reject(new Error('Contract is not initialized.'))
|
||||
return
|
||||
}
|
||||
console.log(wallet.address, contractAddress, codeId)
|
||||
openEditionMinter.migrate(wallet.address, contractAddress, codeId, migrateMsg).then(resolve).catch(reject)
|
||||
})
|
||||
},
|
||||
[openEditionMinter, wallet],
|
||||
)
|
||||
|
||||
const use = useCallback(
|
||||
(customAddress = ''): OpenEditionMinterInstance | undefined => {
|
||||
return openEditionMinter?.use(address || customAddress)
|
||||
},
|
||||
[openEditionMinter, address],
|
||||
)
|
||||
|
||||
const getContractAddress = (): string | undefined => {
|
||||
return address
|
||||
}
|
||||
|
||||
const messages = useCallback(
|
||||
(customAddress = ''): OpenEditionMinterMessages | undefined => {
|
||||
return openEditionMinter?.messages(address || customAddress)
|
||||
},
|
||||
[openEditionMinter, address],
|
||||
)
|
||||
|
||||
return {
|
||||
instantiate,
|
||||
use,
|
||||
updateContractAddress,
|
||||
getContractAddress,
|
||||
messages,
|
||||
migrate,
|
||||
}
|
||||
}
|
3
env.d.ts
vendored
3
env.d.ts
vendored
@ -21,6 +21,9 @@ declare namespace NodeJS {
|
||||
readonly NEXT_PUBLIC_VENDING_MINTER_CODE_ID: string
|
||||
readonly NEXT_PUBLIC_VENDING_MINTER_FLEX_CODE_ID: string
|
||||
readonly NEXT_PUBLIC_VENDING_FACTORY_ADDRESS: string
|
||||
readonly NEXT_PUBLIC_OPEN_EDITION_FACTORY_ADDRESS: string
|
||||
readonly NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS: string
|
||||
readonly NEXT_PUBLIC_OPEN_EDITION_MINTER_CODE_ID: string
|
||||
readonly NEXT_PUBLIC_VENDING_FACTORY_UPDATABLE_ADDRESS: string
|
||||
readonly NEXT_PUBLIC_VENDING_FACTORY_FLEX_ADDRESS: string
|
||||
readonly NEXT_PUBLIC_BASE_FACTORY_ADDRESS: string
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "stargaze-studio",
|
||||
"version": "0.6.4",
|
||||
"version": "0.6.5",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
|
@ -19,7 +19,12 @@ import { links } from 'utils/links'
|
||||
import type { MinterType, Sg721Type } from '../../components/collections/actions/Combobox'
|
||||
|
||||
const CollectionActionsPage: NextPage = () => {
|
||||
const { baseMinter: baseMinterContract, vendingMinter: vendingMinterContract, sg721: sg721Contract } = useContracts()
|
||||
const {
|
||||
baseMinter: baseMinterContract,
|
||||
vendingMinter: vendingMinterContract,
|
||||
openEditionMinter: openEditionMinterContract,
|
||||
sg721: sg721Contract,
|
||||
} = useContracts()
|
||||
const wallet = useWallet()
|
||||
|
||||
const [action, setAction] = useState<boolean>(false)
|
||||
@ -51,6 +56,11 @@ const CollectionActionsPage: NextPage = () => {
|
||||
() => baseMinterContract?.use(minterContractState.value),
|
||||
[baseMinterContract, minterContractState.value],
|
||||
)
|
||||
const openEditionMinterMessages = useMemo(
|
||||
() => openEditionMinterContract?.use(minterContractState.value),
|
||||
[openEditionMinterContract, minterContractState.value],
|
||||
)
|
||||
|
||||
const sg721Messages = useMemo(
|
||||
() => sg721Contract?.use(sg721ContractState.value),
|
||||
[sg721Contract, sg721ContractState.value],
|
||||
@ -105,6 +115,8 @@ const CollectionActionsPage: NextPage = () => {
|
||||
.then((contract) => {
|
||||
if (contract?.includes('sg-base-minter')) {
|
||||
setMinterType('base')
|
||||
} else if (contract?.includes('open-edition')) {
|
||||
setMinterType('openEdition')
|
||||
} else {
|
||||
setMinterType('vending')
|
||||
}
|
||||
@ -214,6 +226,7 @@ const CollectionActionsPage: NextPage = () => {
|
||||
baseMinterMessages={baseMinterMessages}
|
||||
minterContractAddress={minterContractState.value}
|
||||
minterType={minterType}
|
||||
openEditionMinterMessages={openEditionMinterMessages}
|
||||
sg721ContractAddress={sg721ContractState.value}
|
||||
sg721Messages={sg721Messages}
|
||||
sg721Type={sg721Type}
|
||||
@ -224,6 +237,7 @@ const CollectionActionsPage: NextPage = () => {
|
||||
baseMinterMessages={baseMinterMessages}
|
||||
minterContractAddress={minterContractState.value}
|
||||
minterType={minterType}
|
||||
openEditionMinterMessages={openEditionMinterMessages}
|
||||
sg721ContractAddress={sg721ContractState.value}
|
||||
sg721Messages={sg721Messages}
|
||||
vendingMinterMessages={vendingMinterMessages}
|
||||
|
@ -30,6 +30,8 @@ import type { UploadDetailsDataProps } from 'components/collections/creation/Upl
|
||||
import type { WhitelistDetailsDataProps } from 'components/collections/creation/WhitelistDetails'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { LoadingModal } from 'components/LoadingModal'
|
||||
import type { OpenEditionMinterCreatorDataProps } from 'components/openEdition/OpenEditionMinterCreator'
|
||||
import { OpenEditionMinterCreator } from 'components/openEdition/OpenEditionMinterCreator'
|
||||
import { useContracts } from 'contexts/contracts'
|
||||
import { addLogItem } from 'contexts/log'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
@ -48,6 +50,8 @@ import {
|
||||
BASE_FACTORY_UPDATABLE_ADDRESS,
|
||||
BLOCK_EXPLORER_URL,
|
||||
NETWORK,
|
||||
OPEN_EDITION_FACTORY_ADDRESS,
|
||||
OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS,
|
||||
SG721_CODE_ID,
|
||||
SG721_UPDATABLE_CODE_ID,
|
||||
STARGAZE_URL,
|
||||
@ -91,6 +95,9 @@ const CollectionCreationPage: NextPage = () => {
|
||||
const [uploadDetails, setUploadDetails] = useState<UploadDetailsDataProps | null>(null)
|
||||
const [collectionDetails, setCollectionDetails] = useState<CollectionDetailsDataProps | null>(null)
|
||||
const [baseMinterDetails, setBaseMinterDetails] = useState<BaseMinterDetailsDataProps | null>(null)
|
||||
const [openEditionMinterDetails, setOpenEditionMinterDetails] = useState<OpenEditionMinterCreatorDataProps | null>(
|
||||
null,
|
||||
)
|
||||
const [mintingDetails, setMintingDetails] = useState<MintingDetailsDataProps | null>(null)
|
||||
const [whitelistDetails, setWhitelistDetails] = useState<WhitelistDetailsDataProps | null>(null)
|
||||
const [royaltyDetails, setRoyaltyDetails] = useState<RoyaltyDetailsDataProps | null>(null)
|
||||
@ -99,10 +106,16 @@ const CollectionCreationPage: NextPage = () => {
|
||||
const [vendingMinterCreationFee, setVendingMinterCreationFee] = useState<string | null>(null)
|
||||
const [baseMinterCreationFee, setBaseMinterCreationFee] = useState<string | null>(null)
|
||||
const [vendingMinterUpdatableCreationFee, setVendingMinterUpdatableCreationFee] = useState<string | null>(null)
|
||||
const [openEditionMinterCreationFee, setOpenEditionMinterCreationFee] = useState<string | null>(null)
|
||||
const [openEditionMinterUpdatableCreationFee, setOpenEditionMinterUpdatableCreationFee] = useState<string | null>(
|
||||
null,
|
||||
)
|
||||
const [vendingMinterFlexCreationFee, setVendingMinterFlexCreationFee] = useState<string | null>(null)
|
||||
const [baseMinterUpdatableCreationFee, setBaseMinterUpdatableCreationFee] = useState<string | null>(null)
|
||||
const [minimumMintPrice, setMinimumMintPrice] = useState<string | null>('0')
|
||||
const [minimumUpdatableMintPrice, setMinimumUpdatableMintPrice] = useState<string | null>('0')
|
||||
const [minimumOpenEditionMintPrice, setMinimumOpenEditionMintPrice] = useState<string | null>('0')
|
||||
const [minimumOpenEditionUpdatableMintPrice, setMinimumOpenEditionUpdatableMintPrice] = useState<string | null>('0')
|
||||
const [minimumFlexMintPrice, setMinimumFlexMintPrice] = useState<string | null>('0')
|
||||
|
||||
const [uploading, setUploading] = useState(false)
|
||||
@ -1031,6 +1044,26 @@ const CollectionCreationPage: NextPage = () => {
|
||||
setVendingMinterFlexCreationFee(vendingFactoryFlexParameters?.params?.creation_fee?.amount)
|
||||
setMinimumFlexMintPrice(vendingFactoryFlexParameters?.params?.min_mint_price?.amount)
|
||||
}
|
||||
if (OPEN_EDITION_FACTORY_ADDRESS) {
|
||||
const openEditionFactoryParameters = await client
|
||||
.queryContractSmart(OPEN_EDITION_FACTORY_ADDRESS, { params: {} })
|
||||
.catch((error) => {
|
||||
toast.error(`${error.message}`, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
})
|
||||
setOpenEditionMinterCreationFee(openEditionFactoryParameters?.params?.creation_fee?.amount)
|
||||
setMinimumOpenEditionMintPrice(openEditionFactoryParameters?.params?.min_mint_price?.amount)
|
||||
}
|
||||
if (OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS) {
|
||||
const openEditionUpdatableFactoryParameters = await client
|
||||
.queryContractSmart(OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS, { params: {} })
|
||||
.catch((error) => {
|
||||
toast.error(`${error.message}`, { style: { maxWidth: 'none' } })
|
||||
addLogItem({ id: uid(), message: error.message, type: 'Error', timestamp: new Date() })
|
||||
})
|
||||
setOpenEditionMinterUpdatableCreationFee(openEditionUpdatableFactoryParameters?.params?.creation_fee?.amount)
|
||||
setMinimumOpenEditionMintPrice(openEditionUpdatableFactoryParameters?.params?.min_mint_price?.amount)
|
||||
}
|
||||
}
|
||||
|
||||
const checkwalletBalance = () => {
|
||||
@ -1069,9 +1102,13 @@ const CollectionCreationPage: NextPage = () => {
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (vendingMinterContractAddress !== null || isMintingComplete)
|
||||
if (
|
||||
vendingMinterContractAddress !== null ||
|
||||
openEditionMinterDetails?.openEditionMinterContractAddress ||
|
||||
isMintingComplete
|
||||
)
|
||||
scrollRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [vendingMinterContractAddress, isMintingComplete])
|
||||
}, [vendingMinterContractAddress, openEditionMinterDetails?.openEditionMinterContractAddress, isMintingComplete])
|
||||
|
||||
useEffect(() => {
|
||||
setBaseTokenUri(uploadDetails?.baseTokenURI as string)
|
||||
@ -1118,6 +1155,67 @@ const CollectionCreationPage: NextPage = () => {
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-10" ref={scrollRef}>
|
||||
<Conditional
|
||||
test={minterType === 'openEdition' && openEditionMinterDetails?.openEditionMinterContractAddress !== null}
|
||||
>
|
||||
<Alert className="mt-5" type="info">
|
||||
<div>
|
||||
Open Edition Minter Contract Address:{' '}
|
||||
<Anchor
|
||||
className="text-stargaze hover:underline"
|
||||
external
|
||||
href={`/contracts/openEditionMinter/query/?contractAddress=${
|
||||
openEditionMinterDetails?.openEditionMinterContractAddress as string
|
||||
}`}
|
||||
>
|
||||
{openEditionMinterDetails?.openEditionMinterContractAddress as string}
|
||||
</Anchor>
|
||||
<br />
|
||||
SG721 Contract Address:{' '}
|
||||
<Anchor
|
||||
className="text-stargaze hover:underline"
|
||||
external
|
||||
href={`/contracts/sg721/query/?contractAddress=${
|
||||
openEditionMinterDetails?.sg721ContractAddress as string
|
||||
}`}
|
||||
>
|
||||
{openEditionMinterDetails?.sg721ContractAddress as string}
|
||||
</Anchor>
|
||||
<br />
|
||||
Transaction Hash: {' '}
|
||||
<Conditional test={NETWORK === 'testnet'}>
|
||||
<Anchor
|
||||
className="text-stargaze hover:underline"
|
||||
external
|
||||
href={`${BLOCK_EXPLORER_URL}/tx/${openEditionMinterDetails?.transactionHash as string}`}
|
||||
>
|
||||
{openEditionMinterDetails?.transactionHash}
|
||||
</Anchor>
|
||||
</Conditional>
|
||||
<Conditional test={NETWORK === 'mainnet'}>
|
||||
<Anchor
|
||||
className="text-stargaze hover:underline"
|
||||
external
|
||||
href={`${BLOCK_EXPLORER_URL}/txs/${openEditionMinterDetails?.transactionHash as string}`}
|
||||
>
|
||||
{openEditionMinterDetails?.transactionHash}
|
||||
</Anchor>
|
||||
</Conditional>
|
||||
<br />
|
||||
<Button className="mt-2">
|
||||
<Anchor
|
||||
className="text-white"
|
||||
external
|
||||
href={`${STARGAZE_URL}/launchpad/${
|
||||
openEditionMinterDetails?.openEditionMinterContractAddress as string
|
||||
}`}
|
||||
>
|
||||
View on Launchpad
|
||||
</Anchor>
|
||||
</Button>
|
||||
</div>
|
||||
</Alert>
|
||||
</Conditional>
|
||||
<Conditional test={vendingMinterContractAddress !== null || isMintingComplete}>
|
||||
<Alert className="mt-5" type="info">
|
||||
<div>
|
||||
@ -1289,80 +1387,108 @@ const CollectionCreationPage: NextPage = () => {
|
||||
</Conditional>
|
||||
</div>
|
||||
|
||||
{/* To be removed */}
|
||||
<Conditional test={BASE_FACTORY_ADDRESS === undefined}>
|
||||
<div className="mx-10 mt-5" />
|
||||
</Conditional>
|
||||
<Conditional test={BASE_FACTORY_ADDRESS !== undefined}>
|
||||
{/* /To be removed */}
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
className={clsx(
|
||||
'mx-10 mt-5',
|
||||
'grid before:absolute relative grid-cols-3 grid-flow-col items-stretch rounded',
|
||||
'before:inset-x-0 before:bottom-0 before:border-white/25',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'mx-10 mt-5',
|
||||
'grid before:absolute relative grid-cols-2 grid-flow-col items-stretch rounded',
|
||||
'before:inset-x-0 before:bottom-0 before:border-white/25',
|
||||
'isolate space-y-1 border-2',
|
||||
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
||||
minterType === 'vending' ? 'border-stargaze' : 'border-transparent',
|
||||
minterType !== 'vending' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'isolate space-y-1 border-2',
|
||||
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
||||
minterType === 'vending' ? 'border-stargaze' : 'border-transparent',
|
||||
minterType !== 'vending' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
||||
)}
|
||||
<button
|
||||
className="p-4 w-full h-full text-left bg-transparent"
|
||||
onClick={() => {
|
||||
setMinterType('vending')
|
||||
resetReadyFlags()
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<button
|
||||
className="p-4 w-full h-full text-left bg-transparent"
|
||||
onClick={() => {
|
||||
setMinterType('vending')
|
||||
resetReadyFlags()
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<h4 className="font-bold">Standard Collection</h4>
|
||||
<span className="text-sm text-white/80 line-clamp-2">
|
||||
A non-appendable collection that facilitates primary market vending machine style minting
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'isolate space-y-1 border-2',
|
||||
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
||||
minterType === 'base' ? 'border-stargaze' : 'border-transparent',
|
||||
minterType !== 'base' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
||||
)}
|
||||
<h4 className="font-bold">Standard Collection</h4>
|
||||
<span className="text-sm text-white/80 line-clamp-2">
|
||||
A non-appendable collection that facilitates primary market vending machine style minting
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'isolate space-y-1 border-2',
|
||||
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
||||
minterType === 'base' ? 'border-stargaze' : 'border-transparent',
|
||||
minterType !== 'base' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className="p-4 w-full h-full text-left bg-transparent"
|
||||
onClick={() => {
|
||||
setMinterType('base')
|
||||
resetReadyFlags()
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<button
|
||||
className="p-4 w-full h-full text-left bg-transparent"
|
||||
onClick={() => {
|
||||
setMinterType('base')
|
||||
resetReadyFlags()
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<h4 className="font-bold">1/1 Collection</h4>
|
||||
<span className="text-sm text-white/80 line-clamp-2">
|
||||
An appendable collection that only allows for direct secondary market listing of tokens
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<h4 className="font-bold">1/1 Collection</h4>
|
||||
<span className="text-sm text-white/80 line-clamp-2">
|
||||
An appendable collection that only allows for direct secondary market listing of tokens
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'isolate space-y-1 border-2',
|
||||
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
||||
minterType === 'openEdition' ? 'border-stargaze' : 'border-transparent',
|
||||
minterType !== 'openEdition' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
||||
OPEN_EDITION_FACTORY_ADDRESS === undefined ? 'hover:bg-zinc-500 opacity-50 hover:opacity-70' : '',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className="p-4 w-full h-full text-left bg-transparent"
|
||||
disabled={OPEN_EDITION_FACTORY_ADDRESS === undefined}
|
||||
onClick={() => {
|
||||
setMinterType('openEdition')
|
||||
resetReadyFlags()
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<h4 className="font-bold">Open Edition Collection</h4>
|
||||
<span className="text-sm text-white/80 line-clamp-2">
|
||||
Allows multiple copies of a single NFT to be minted for a given time interval
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Conditional>
|
||||
</div>
|
||||
|
||||
{minterType === 'base' && (
|
||||
<div>
|
||||
<BaseMinterDetails minterType={minterType} onChange={setBaseMinterDetails} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mx-10">
|
||||
<UploadDetails
|
||||
baseMinterAcquisitionMethod={baseMinterDetails?.baseMinterAcquisitionMethod}
|
||||
<Conditional test={minterType === 'openEdition'}>
|
||||
<OpenEditionMinterCreator
|
||||
minimumMintPrice={minimumOpenEditionMintPrice as string}
|
||||
minimumUpdatableMintPrice={minimumOpenEditionUpdatableMintPrice as string}
|
||||
minterType={minterType}
|
||||
onChange={setUploadDetails}
|
||||
onChange={setOpenEditionMinterDetails}
|
||||
openEditionMinterCreationFee={openEditionMinterCreationFee as string}
|
||||
openEditionMinterUpdatableCreationFee={openEditionMinterUpdatableCreationFee as string}
|
||||
/>
|
||||
</Conditional>
|
||||
<div className="mx-10">
|
||||
<Conditional test={minterType === 'vending' || minterType === 'base'}>
|
||||
<UploadDetails
|
||||
baseMinterAcquisitionMethod={baseMinterDetails?.baseMinterAcquisitionMethod}
|
||||
minterType={minterType}
|
||||
onChange={setUploadDetails}
|
||||
/>
|
||||
</Conditional>
|
||||
|
||||
<Conditional
|
||||
test={
|
||||
|
@ -27,6 +27,7 @@ const CollectionList: NextPage = () => {
|
||||
const [myCollections, setMyCollections] = useState<any[]>([])
|
||||
const [myOneOfOneCollections, setMyOneOfOneCollections] = useState<any[]>([])
|
||||
const [myStandardCollections, setMyStandardCollections] = useState<any[]>([])
|
||||
const [myOpenEditionCollections, setMyOpenEditionCollections] = useState<any[]>([])
|
||||
|
||||
async function getMinterContractType(minterContractAddress: string) {
|
||||
if (wallet.client && minterContractAddress.length > 0) {
|
||||
@ -43,6 +44,7 @@ const CollectionList: NextPage = () => {
|
||||
const filterMyCollections = () => {
|
||||
setMyOneOfOneCollections([])
|
||||
setMyStandardCollections([])
|
||||
setMyOpenEditionCollections([])
|
||||
if (myCollections.length > 0) {
|
||||
myCollections.map(async (collection: any) => {
|
||||
await getMinterContractType(collection.minter)
|
||||
@ -51,6 +53,8 @@ const CollectionList: NextPage = () => {
|
||||
setMyOneOfOneCollections((prevState) => [...prevState, collection])
|
||||
} else if (contractType?.includes('sg-minter') || contractType?.includes('flex')) {
|
||||
setMyStandardCollections((prevState) => [...prevState, collection])
|
||||
} else if (contractType?.includes('open-edition')) {
|
||||
setMyOpenEditionCollections((prevState) => [...prevState, collection])
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
@ -302,11 +306,120 @@ const CollectionList: NextPage = () => {
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{myOpenEditionCollections.length > 0 && (
|
||||
<div className="bg-transparent">
|
||||
<span className="ml-6 text-2xl font-bold text-blue-300 underline underline-offset-4">
|
||||
Open Edition Collections
|
||||
</span>
|
||||
<table className="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="pl-36 text-lg font-bold text-left bg-transparent">Collection Name</th>
|
||||
<th className="text-lg font-bold bg-transparent">Contract Address</th>
|
||||
<th className="bg-transparent" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{myOpenEditionCollections.map((collection: any, index: any) => {
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="w-[40%] bg-black">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="avatar">
|
||||
<div className="w-28 h-28 mask mask-squircle">
|
||||
<img
|
||||
alt="Cover"
|
||||
src={
|
||||
(collection?.image as string).startsWith('ipfs')
|
||||
? `https://ipfs-gw.stargaze-apis.com/ipfs/${(
|
||||
collection?.image as string
|
||||
).substring(7)}`
|
||||
: collection?.image
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pl-2">
|
||||
<p className="overflow-auto font-bold lg:max-w-[160px] xl:max-w-[220px] 2xl:max-w-xs no-scrollbar ">
|
||||
{collection.name}
|
||||
</p>
|
||||
<p className="text-sm truncate opacity-50 lg:max-w-[160px] xl:max-w-[220px] 2xl:max-w-xs">
|
||||
{collection.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="w-[50%] bg-black">
|
||||
<div className="flex flex-row items-center space-x-3">
|
||||
Minter:
|
||||
<span className="ml-2">
|
||||
<Tooltip
|
||||
backgroundColor="bg-blue-500"
|
||||
label="Click to copy the Vending Minter contract address"
|
||||
>
|
||||
<button
|
||||
className="group flex space-x-2 font-mono text-base text-white/80 hover:underline"
|
||||
onClick={() => void copy(collection.minter as string)}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
{truncateMiddle(collection.minter ? (collection.minter as string) : '', 36)}
|
||||
</span>
|
||||
<FaCopy className="opacity-0 group-hover:opacity-100" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-row items-center space-x-3">
|
||||
SG721:
|
||||
<span className="ml-2">
|
||||
<Tooltip backgroundColor="bg-blue-500" label="Click to copy the SG721 contract address">
|
||||
<button
|
||||
className="group flex space-x-2 font-mono text-base text-white/80 hover:underline"
|
||||
onClick={() => void copy(collection.contractAddress as string)}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
{truncateMiddle(
|
||||
collection.contractAddress ? (collection.contractAddress as string) : '',
|
||||
36,
|
||||
)}
|
||||
</span>
|
||||
<FaCopy className="opacity-0 group-hover:opacity-100" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<th className="bg-black">
|
||||
<div className="flex items-center space-x-8">
|
||||
<Anchor
|
||||
className="text-xl text-plumbus"
|
||||
href={`/collections/actions?sg721ContractAddress=${collection.contractAddress}&minterContractAddress=${collection.minter}`}
|
||||
>
|
||||
<FaSlidersH />
|
||||
</Anchor>
|
||||
<Anchor
|
||||
className="text-xl text-plumbus"
|
||||
external
|
||||
href={`${STARGAZE_URL}/launchpad/${collection.minter}`}
|
||||
>
|
||||
<FaRocket />
|
||||
</Anchor>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}, [myCollections, myStandardCollections, myOneOfOneCollections, wallet.address])
|
||||
}, [myCollections, myStandardCollections, myOneOfOneCollections, myOpenEditionCollections, wallet.address])
|
||||
|
||||
return (
|
||||
<section className="py-6 px-12 space-y-4">
|
||||
|
@ -100,7 +100,7 @@ const BaseMinterQueryPage: NextPage = () => {
|
||||
onChange={(e) => setType(e.target.value as QueryType)}
|
||||
>
|
||||
{QUERY_LIST.map(({ id, name }) => (
|
||||
<option key={`query-${id}`} value={id}>
|
||||
<option key={`query-${id}`} className="mt-2 text-lg bg-[#1A1A1A]" value={id}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
|
@ -4,7 +4,7 @@ import type { NextPage } from 'next'
|
||||
// import Brand from 'public/brand/brand.svg'
|
||||
import { withMetadata } from 'utils/layout'
|
||||
|
||||
import { BADGE_HUB_ADDRESS, BASE_FACTORY_ADDRESS } from '../../utils/constants'
|
||||
import { BADGE_HUB_ADDRESS, BASE_FACTORY_ADDRESS, OPEN_EDITION_FACTORY_ADDRESS } from '../../utils/constants'
|
||||
|
||||
const HomePage: NextPage = () => {
|
||||
return (
|
||||
@ -42,6 +42,15 @@ const HomePage: NextPage = () => {
|
||||
<HomeCard className="p-4 -m-4 hover:bg-gray-500/10 rounded" link="/contracts/sg721" title="Sg721 Contract">
|
||||
Execute messages and run queries on Stargaze's SG721 contract.
|
||||
</HomeCard>
|
||||
<Conditional test={OPEN_EDITION_FACTORY_ADDRESS !== undefined}>
|
||||
<HomeCard
|
||||
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
|
||||
link="/contracts/openEditionMinter"
|
||||
title="Open Edition Minter Contract"
|
||||
>
|
||||
Execute messages and run queries on Stargaze's Open Edition Minter contract.
|
||||
</HomeCard>
|
||||
</Conditional>
|
||||
<HomeCard
|
||||
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
|
||||
link="/contracts/whitelist"
|
||||
|
249
pages/contracts/openEditionMinter/execute.tsx
Normal file
249
pages/contracts/openEditionMinter/execute.tsx
Normal file
@ -0,0 +1,249 @@
|
||||
import { Button } from 'components/Button'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||
import { ExecuteCombobox } from 'components/contracts/openEditionMinter/ExecuteCombobox'
|
||||
import { useExecuteComboboxState } from 'components/contracts/openEditionMinter/ExecuteCombobox.hooks'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
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 { LinkTabs } from 'components/LinkTabs'
|
||||
import { openEditionMinterLinkTabs } from 'components/LinkTabs.data'
|
||||
import { TransactionHash } from 'components/TransactionHash'
|
||||
import { useContracts } from 'contexts/contracts'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { DispatchExecuteArgs } from 'contracts/openEditionMinter/messages/execute'
|
||||
import { dispatchExecute, isEitherType, previewExecutePayload } from 'contracts/openEditionMinter/messages/execute'
|
||||
import type { NextPage } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import type { FormEvent } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { FaArrowRight } from 'react-icons/fa'
|
||||
import { useMutation } from 'react-query'
|
||||
import { withMetadata } from 'utils/layout'
|
||||
import { links } from 'utils/links'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
|
||||
const OpenEditionMinterExecutePage: NextPage = () => {
|
||||
const { openEditionMinter: contract } = useContracts()
|
||||
const wallet = useWallet()
|
||||
const [lastTx, setLastTx] = useState('')
|
||||
|
||||
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
||||
const [endTimestamp, setEndTimestamp] = useState<Date | undefined>(undefined)
|
||||
const [resolvedRecipientAddress, setResolvedRecipientAddress] = useState<string>('')
|
||||
|
||||
const comboboxState = useExecuteComboboxState()
|
||||
const type = comboboxState.value?.id
|
||||
|
||||
const limitState = useNumberInputState({
|
||||
id: 'per-address-limit',
|
||||
name: 'perAddressLimit',
|
||||
title: 'Per Address Limit',
|
||||
subtitle: 'Enter the per address limit',
|
||||
})
|
||||
|
||||
const tokenIdState = useNumberInputState({
|
||||
id: 'token-id',
|
||||
name: 'tokenId',
|
||||
title: 'Token ID',
|
||||
subtitle: 'Enter the token ID',
|
||||
})
|
||||
|
||||
const priceState = useNumberInputState({
|
||||
id: 'price',
|
||||
name: 'price',
|
||||
title: 'Price',
|
||||
subtitle: 'Enter the price for each token',
|
||||
})
|
||||
|
||||
const contractState = useInputState({
|
||||
id: 'contract-address',
|
||||
name: 'contract-address',
|
||||
title: 'Open Edition Minter Address',
|
||||
subtitle: 'Address of the Open Edition Minter contract',
|
||||
})
|
||||
const contractAddress = contractState.value
|
||||
|
||||
const recipientState = useInputState({
|
||||
id: 'recipient-address',
|
||||
name: 'recipient',
|
||||
title: 'Recipient Address',
|
||||
subtitle: 'Address of the recipient',
|
||||
})
|
||||
|
||||
const showDateField = isEitherType(type, ['update_start_time', 'update_start_trading_time'])
|
||||
const showEndDateField = type === 'update_end_time'
|
||||
const showLimitField = type === 'update_per_address_limit'
|
||||
const showRecipientField = isEitherType(type, ['mint_to'])
|
||||
const showPriceField = isEitherType(type, ['update_mint_price'])
|
||||
|
||||
const messages = useMemo(() => contract?.use(contractState.value), [contract, wallet.address, contractState.value])
|
||||
const payload: DispatchExecuteArgs = {
|
||||
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
|
||||
endTime: endTimestamp ? (endTimestamp.getTime() * 1_000_000).toString() : '',
|
||||
limit: limitState.value,
|
||||
contract: contractState.value,
|
||||
messages,
|
||||
recipient: resolvedRecipientAddress,
|
||||
txSigner: wallet.address,
|
||||
price: priceState.value ? priceState.value.toString() : '0',
|
||||
type,
|
||||
}
|
||||
const { isLoading, mutate } = useMutation(
|
||||
async (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
if (!type) {
|
||||
throw new Error('Please select message type!')
|
||||
}
|
||||
if (!wallet.initialized) {
|
||||
throw new Error('Please connect your wallet.')
|
||||
}
|
||||
if (contractState.value === '') {
|
||||
throw new Error('Please enter the contract address.')
|
||||
}
|
||||
|
||||
if (wallet.client && type === 'update_mint_price') {
|
||||
const contractConfig = wallet.client.queryContractSmart(contractState.value, {
|
||||
config: {},
|
||||
})
|
||||
await toast
|
||||
.promise(
|
||||
wallet.client.queryContractSmart(contractState.value, {
|
||||
mint_price: {},
|
||||
}),
|
||||
{
|
||||
error: `Querying mint price failed!`,
|
||||
loading: 'Querying current mint price...',
|
||||
success: (price) => {
|
||||
console.log(price)
|
||||
return `Current mint price is ${Number(price.public_price.amount) / 1000000} STARS`
|
||||
},
|
||||
},
|
||||
)
|
||||
.then(async (price) => {
|
||||
if (Number(price.public_price.amount) / 1000000 <= priceState.value) {
|
||||
await contractConfig
|
||||
.then((config) => {
|
||||
console.log(config.start_time, Date.now() * 1000000)
|
||||
if (Number(config.start_time) < Date.now() * 1000000) {
|
||||
throw new Error(
|
||||
`Minting has already started on ${new Date(
|
||||
Number(config.start_time) / 1000000,
|
||||
).toLocaleString()}. Updated mint price cannot be higher than the current price of ${
|
||||
Number(price.public_price.amount) / 1000000
|
||||
} STARS`,
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
throw new Error(String(error).substring(String(error).lastIndexOf('Error:') + 7))
|
||||
})
|
||||
} else {
|
||||
await contractConfig.then(async (config) => {
|
||||
const factoryParameters = await wallet.client?.queryContractSmart(config.factory, {
|
||||
params: {},
|
||||
})
|
||||
if (
|
||||
factoryParameters.params.min_mint_price.amount &&
|
||||
priceState.value < Number(factoryParameters.params.min_mint_price.amount) / 1000000
|
||||
) {
|
||||
throw new Error(
|
||||
`Updated mint price cannot be lower than the minimum mint price of ${
|
||||
Number(factoryParameters.params.min_mint_price.amount) / 1000000
|
||||
} STARS`,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const txHash = await toast.promise(dispatchExecute(payload), {
|
||||
error: `${type.charAt(0).toUpperCase() + type.slice(1)} execute failed!`,
|
||||
loading: 'Executing message...',
|
||||
success: (tx) => `Transaction ${tx} success!`,
|
||||
})
|
||||
if (txHash) {
|
||||
setLastTx(txHash)
|
||||
}
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(String(error), { style: { maxWidth: 'none' } })
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (contractAddress.length > 0) {
|
||||
void router.replace({ query: { contractAddress } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [contractAddress])
|
||||
useEffect(() => {
|
||||
const initial = new URL(document.URL).searchParams.get('contractAddress')
|
||||
if (initial && initial.length > 0) contractState.onChange(initial)
|
||||
}, [])
|
||||
|
||||
const resolveRecipientAddress = async () => {
|
||||
await resolveAddress(recipientState.value.trim(), wallet).then((resolvedAddress) => {
|
||||
setResolvedRecipientAddress(resolvedAddress)
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
void resolveRecipientAddress()
|
||||
}, [recipientState.value])
|
||||
|
||||
return (
|
||||
<section className="py-6 px-12 space-y-4">
|
||||
<NextSeo title="Execute Open Edition Minter Contract" />
|
||||
<ContractPageHeader
|
||||
description="Open Edition Minter contract allows multiple copies of a single NFT to be minted in a specified time interval."
|
||||
link={links.Documentation}
|
||||
title="Open Edition Minter Contract"
|
||||
/>
|
||||
<LinkTabs activeIndex={1} data={openEditionMinterLinkTabs} />
|
||||
|
||||
<form className="grid grid-cols-2 p-4 space-x-8" onSubmit={mutate}>
|
||||
<div className="space-y-8">
|
||||
<AddressInput {...contractState} />
|
||||
<ExecuteCombobox {...comboboxState} />
|
||||
{showRecipientField && <AddressInput {...recipientState} />}
|
||||
{showLimitField && <NumberInput {...limitState} />}
|
||||
{showPriceField && <NumberInput {...priceState} />}
|
||||
{/* TODO: Fix address execute message */}
|
||||
<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} />
|
||||
</FormControl>
|
||||
</Conditional>
|
||||
<Conditional test={showEndDateField}>
|
||||
<FormControl htmlId="end-date" subtitle="End time for the minting" title="End Time">
|
||||
<InputDateTime minDate={new Date()} onChange={(date) => setEndTimestamp(date)} value={endTimestamp} />
|
||||
</FormControl>
|
||||
</Conditional>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="relative">
|
||||
<Button className="absolute top-0 right-0" isLoading={isLoading} rightIcon={<FaArrowRight />} type="submit">
|
||||
Execute
|
||||
</Button>
|
||||
<FormControl subtitle="View execution transaction hash" title="Transaction Hash">
|
||||
<TransactionHash hash={lastTx} />
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormControl subtitle="View current message to be sent" title="Payload Preview">
|
||||
<JsonPreview content={previewExecutePayload(payload)} isCopyable />
|
||||
</FormControl>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default withMetadata(OpenEditionMinterExecutePage, { center: false })
|
1
pages/contracts/openEditionMinter/index.tsx
Normal file
1
pages/contracts/openEditionMinter/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './query'
|
132
pages/contracts/openEditionMinter/migrate.tsx
Normal file
132
pages/contracts/openEditionMinter/migrate.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
import { Button } from 'components/Button'
|
||||
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||
import { useExecuteComboboxState } from 'components/contracts/openEditionMinter/ExecuteCombobox.hooks'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { AddressInput, NumberInput } from 'components/forms/FormInput'
|
||||
import { useInputState, useNumberInputState } from 'components/forms/FormInput.hooks'
|
||||
import { JsonPreview } from 'components/JsonPreview'
|
||||
import { LinkTabs } from 'components/LinkTabs'
|
||||
import { openEditionMinterLinkTabs } from 'components/LinkTabs.data'
|
||||
import { TransactionHash } from 'components/TransactionHash'
|
||||
import { useContracts } from 'contexts/contracts'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { MigrateResponse } from 'contracts/openEditionMinter'
|
||||
import type { NextPage } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import type { FormEvent } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { FaArrowRight } from 'react-icons/fa'
|
||||
import { useMutation } from 'react-query'
|
||||
import { withMetadata } from 'utils/layout'
|
||||
import { links } from 'utils/links'
|
||||
|
||||
const OpenEditionMinterMigratePage: NextPage = () => {
|
||||
const { openEditionMinter: contract } = useContracts()
|
||||
const wallet = useWallet()
|
||||
|
||||
const [lastTx, setLastTx] = useState('')
|
||||
|
||||
const comboboxState = useExecuteComboboxState()
|
||||
const type = comboboxState.value?.id
|
||||
const codeIdState = useNumberInputState({
|
||||
id: 'code-id',
|
||||
name: 'code-id',
|
||||
title: 'Code ID',
|
||||
subtitle: 'Code ID of the New Open Edition Minter',
|
||||
placeholder: '1',
|
||||
})
|
||||
|
||||
const contractState = useInputState({
|
||||
id: 'contract-address',
|
||||
name: 'contract-address',
|
||||
title: 'Open Edition Minter Address',
|
||||
subtitle: 'Address of the Open Edition Minter contract',
|
||||
})
|
||||
const contractAddress = contractState.value
|
||||
|
||||
const { data, isLoading, mutate } = useMutation(
|
||||
async (event: FormEvent): Promise<MigrateResponse | null> => {
|
||||
event.preventDefault()
|
||||
if (!contract) {
|
||||
throw new Error('Smart contract connection failed')
|
||||
}
|
||||
if (!wallet.initialized) {
|
||||
throw new Error('Please connect your wallet.')
|
||||
}
|
||||
|
||||
const migrateMsg = {}
|
||||
return toast.promise(contract.migrate(contractAddress, codeIdState.value, migrateMsg), {
|
||||
error: `Migration failed!`,
|
||||
loading: 'Executing message...',
|
||||
success: (tx) => {
|
||||
if (tx) {
|
||||
setLastTx(tx.transactionHash)
|
||||
}
|
||||
return `Transaction success!`
|
||||
},
|
||||
})
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
toast.error(String(error), { style: { maxWidth: 'none' } })
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (contractAddress.length > 0) {
|
||||
void router.replace({ query: { contractAddress } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [contractAddress])
|
||||
useEffect(() => {
|
||||
const initial = new URL(document.URL).searchParams.get('contractAddress')
|
||||
if (initial && initial.length > 0) contractState.onChange(initial)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section className="py-6 px-12 space-y-4">
|
||||
<NextSeo title="Migrate Open Edition Minter Contract" />
|
||||
<ContractPageHeader
|
||||
description="Open Edition Minter contract allows multiple copies of a single NFT to be minted in a specified time interval."
|
||||
link={links.Documentation}
|
||||
title="Open Edition Minter Contract"
|
||||
/>
|
||||
<LinkTabs activeIndex={2} data={openEditionMinterLinkTabs} />
|
||||
|
||||
<form className="grid grid-cols-2 p-4 space-x-8" onSubmit={mutate}>
|
||||
<div className="space-y-8">
|
||||
<AddressInput {...contractState} />
|
||||
<NumberInput isRequired {...codeIdState} />
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="relative">
|
||||
<Button className="absolute top-0 right-0" isLoading={isLoading} rightIcon={<FaArrowRight />} type="submit">
|
||||
Execute
|
||||
</Button>
|
||||
<FormControl subtitle="View execution transaction hash" title="Transaction Hash">
|
||||
<TransactionHash hash={lastTx} />
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormControl subtitle="View current message to be sent" title="Payload Preview">
|
||||
<JsonPreview
|
||||
content={{
|
||||
sender: wallet.address,
|
||||
contract: contractAddress,
|
||||
code_id: codeIdState.value,
|
||||
msg: {},
|
||||
}}
|
||||
isCopyable
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default withMetadata(OpenEditionMinterMigratePage, { center: false })
|
124
pages/contracts/openEditionMinter/query.tsx
Normal file
124
pages/contracts/openEditionMinter/query.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import clsx from 'clsx'
|
||||
import { Conditional } from 'components/Conditional'
|
||||
import { ContractPageHeader } from 'components/ContractPageHeader'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import { AddressInput } from 'components/forms/FormInput'
|
||||
import { useInputState } from 'components/forms/FormInput.hooks'
|
||||
import { JsonPreview } from 'components/JsonPreview'
|
||||
import { LinkTabs } from 'components/LinkTabs'
|
||||
import { openEditionMinterLinkTabs } from 'components/LinkTabs.data'
|
||||
import { useContracts } from 'contexts/contracts'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { QueryType } from 'contracts/openEditionMinter/messages/query'
|
||||
import { dispatchQuery, QUERY_LIST } from 'contracts/openEditionMinter/messages/query'
|
||||
import type { NextPage } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { useQuery } from 'react-query'
|
||||
import { withMetadata } from 'utils/layout'
|
||||
import { links } from 'utils/links'
|
||||
import { resolveAddress } from 'utils/resolveAddress'
|
||||
|
||||
const OpenEditionMinterQueryPage: NextPage = () => {
|
||||
const { openEditionMinter: contract } = useContracts()
|
||||
const wallet = useWallet()
|
||||
|
||||
const contractState = useInputState({
|
||||
id: 'contract-address',
|
||||
name: 'contract-address',
|
||||
title: 'Open Edition Minter Address',
|
||||
subtitle: 'Address of the Open Edition Minter contract',
|
||||
})
|
||||
const contractAddress = contractState.value
|
||||
|
||||
const addressState = useInputState({
|
||||
id: 'address',
|
||||
name: 'address',
|
||||
title: 'Address',
|
||||
subtitle: 'Address of the user - defaults to current address',
|
||||
})
|
||||
const address = addressState.value
|
||||
|
||||
const [type, setType] = useState<QueryType>('config')
|
||||
|
||||
const { data: response } = useQuery(
|
||||
[contractAddress, type, contract, wallet, address] as const,
|
||||
async ({ queryKey }) => {
|
||||
const [_contractAddress, _type, _contract, _wallet] = queryKey
|
||||
const messages = contract?.use(_contractAddress)
|
||||
const res = await resolveAddress(address, wallet).then(async (resolvedAddress) => {
|
||||
const result = await dispatchQuery({
|
||||
address: resolvedAddress,
|
||||
messages,
|
||||
type,
|
||||
})
|
||||
return result
|
||||
})
|
||||
return res
|
||||
},
|
||||
{
|
||||
placeholderData: null,
|
||||
onError: (error: any) => {
|
||||
toast.error(error.message, { style: { maxWidth: 'none' } })
|
||||
},
|
||||
enabled: Boolean(contractAddress && contract && wallet),
|
||||
},
|
||||
)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (contractAddress.length > 0) {
|
||||
void router.replace({ query: { contractAddress } })
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [contractAddress])
|
||||
useEffect(() => {
|
||||
const initial = new URL(document.URL).searchParams.get('contractAddress')
|
||||
if (initial && initial.length > 0) contractState.onChange(initial)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section className="py-6 px-12 space-y-4">
|
||||
<NextSeo title="Query Open Edition Minter Contract" />
|
||||
<ContractPageHeader
|
||||
description="Open Edition Minter contract allows multiple copies of a single NFT to be minted in a specified time interval."
|
||||
link={links.Documentation}
|
||||
title="Open Edition Minter Contract"
|
||||
/>
|
||||
<LinkTabs activeIndex={0} data={openEditionMinterLinkTabs} />
|
||||
|
||||
<div className="grid grid-cols-2 p-4 space-x-8">
|
||||
<div className="space-y-8">
|
||||
<AddressInput {...contractState} />
|
||||
<FormControl htmlId="contract-query-type" subtitle="Type of query to be dispatched" title="Query Type">
|
||||
<select
|
||||
className={clsx(
|
||||
'bg-white/10 rounded border-2 border-white/20 form-select',
|
||||
'placeholder:text-white/50',
|
||||
'focus:ring focus:ring-plumbus-20',
|
||||
)}
|
||||
id="contract-query-type"
|
||||
name="query-type"
|
||||
onChange={(e) => setType(e.target.value as QueryType)}
|
||||
>
|
||||
{QUERY_LIST.map(({ id, name }) => (
|
||||
<option key={`query-${id}`} className="mt-2 text-lg bg-[#1A1A1A]" value={id}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormControl>
|
||||
<Conditional test={type === 'mint_count'}>
|
||||
<AddressInput {...addressState} />
|
||||
</Conditional>
|
||||
</div>
|
||||
<JsonPreview content={contractAddress ? { type, response } : null} title="Query Response" />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default withMetadata(OpenEditionMinterQueryPage, { center: false })
|
@ -118,7 +118,7 @@ const Sg721QueryPage: NextPage = () => {
|
||||
onChange={(e) => setType(e.target.value as QueryType)}
|
||||
>
|
||||
{QUERY_LIST.map(({ id, name }) => (
|
||||
<option key={`query-${id}`} value={id}>
|
||||
<option key={`query-${id}`} className="mt-2 text-lg bg-[#1A1A1A]" value={id}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
|
@ -105,7 +105,7 @@ const VendingMinterQueryPage: NextPage = () => {
|
||||
onChange={(e) => setType(e.target.value as QueryType)}
|
||||
>
|
||||
{QUERY_LIST.map(({ id, name }) => (
|
||||
<option key={`query-${id}`} value={id}>
|
||||
<option key={`query-${id}`} className="mt-2 text-lg bg-[#1A1A1A]" value={id}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
|
@ -9,6 +9,9 @@ export const VENDING_FACTORY_UPDATABLE_ADDRESS = process.env.NEXT_PUBLIC_VENDING
|
||||
export const VENDING_FACTORY_FLEX_ADDRESS = process.env.NEXT_PUBLIC_VENDING_FACTORY_FLEX_ADDRESS
|
||||
export const BASE_FACTORY_ADDRESS = process.env.NEXT_PUBLIC_BASE_FACTORY_ADDRESS
|
||||
export const BASE_FACTORY_UPDATABLE_ADDRESS = process.env.NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_ADDRESS
|
||||
export const OPEN_EDITION_FACTORY_ADDRESS = process.env.NEXT_PUBLIC_OPEN_EDITION_FACTORY_ADDRESS
|
||||
export const OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS = process.env.NEXT_PUBLIC_OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS
|
||||
export const OPEN_EDITION_MINTER_CODE_ID = parseInt(process.env.NEXT_PUBLIC_OPEN_EDITION_MINTER_CODE_ID, 10)
|
||||
export const SG721_NAME_ADDRESS = process.env.NEXT_PUBLIC_SG721_NAME_ADDRESS
|
||||
export const BASE_MINTER_CODE_ID = parseInt(process.env.NEXT_PUBLIC_VENDING_MINTER_CODE_ID, 10)
|
||||
export const BADGE_HUB_CODE_ID = parseInt(process.env.NEXT_PUBLIC_BADGE_HUB_CODE_ID, 10)
|
||||
|
Loading…
Reference in New Issue
Block a user