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_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS
|
||||||
NEXT_PUBLIC_SG721_CODE_ID=2092
|
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_VENDING_FACTORY_FLEX_ADDRESS="stars1gy6hr9sq9fzrykzw0emmehnjy27agreuepjrfnjnlwlugg29l2qqt0yu2j"
|
||||||
NEXT_PUBLIC_BASE_FACTORY_ADDRESS="stars18kzfpdgx36m95mszchegnk7car4sq03uvg25zeph2j7xg3rk03cs007sxr"
|
NEXT_PUBLIC_BASE_FACTORY_ADDRESS="stars18kzfpdgx36m95mszchegnk7car4sq03uvg25zeph2j7xg3rk03cs007sxr"
|
||||||
NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_ADDRESS="stars13pw8r33dsnghlxfj2upaywf38z2fc6npuw9maq9e5cpet4v285sscgzjp2"
|
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_SG721_NAME_ADDRESS="stars1fx74nkqkw2748av8j7ew7r3xt9cgjqduwn8m0ur5lhe49uhlsasszc5fhr"
|
||||||
NEXT_PUBLIC_WHITELIST_CODE_ID=2093
|
NEXT_PUBLIC_WHITELIST_CODE_ID=2093
|
||||||
NEXT_PUBLIC_WHITELIST_FLEX_CODE_ID=2005
|
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[] = [
|
export const baseMinterLinkTabs: LinkTabProps[] = [
|
||||||
{
|
{
|
||||||
title: 'Instantiate',
|
title: 'Instantiate',
|
||||||
|
@ -175,9 +175,9 @@ export const MetadataInput = (props: MetadataInputProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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="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 {...nameState} />
|
||||||
<TextInput className="mt-2" {...descriptionState} />
|
<TextInput className="mt-2" {...descriptionState} />
|
||||||
<TextInput className="mt-2" {...externalUrlState} />
|
<TextInput className="mt-2" {...externalUrlState} />
|
||||||
|
@ -11,7 +11,7 @@ import { useEffect } from 'react'
|
|||||||
// import BrandText from 'public/brand/brand-text.svg'
|
// import BrandText from 'public/brand/brand-text.svg'
|
||||||
import { footerLinks, socialsLinks } from 'utils/links'
|
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 { Conditional } from './Conditional'
|
||||||
import { IncomeDashboardDisclaimer } from './IncomeDashboardDisclaimer'
|
import { IncomeDashboardDisclaimer } from './IncomeDashboardDisclaimer'
|
||||||
import { LogModal } from './LogModal'
|
import { LogModal } from './LogModal'
|
||||||
@ -175,6 +175,17 @@ export const Sidebar = () => {
|
|||||||
>
|
>
|
||||||
<Link href="/contracts/vendingMinter/">Vending Minter Contract</Link>
|
<Link href="/contracts/vendingMinter/">Vending Minter Contract</Link>
|
||||||
</li>
|
</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
|
<li
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'text-lg font-bold hover:text-white hover:bg-stargaze-80 rounded',
|
'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 { TransactionHash } from 'components/TransactionHash'
|
||||||
import { useWallet } from 'contexts/wallet'
|
import { useWallet } from 'contexts/wallet'
|
||||||
import type { BaseMinterInstance } from 'contracts/baseMinter'
|
import type { BaseMinterInstance } from 'contracts/baseMinter'
|
||||||
|
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter'
|
||||||
import type { SG721Instance } from 'contracts/sg721'
|
import type { SG721Instance } from 'contracts/sg721'
|
||||||
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
||||||
import type { FormEvent } from 'react'
|
import type { FormEvent } from 'react'
|
||||||
@ -35,6 +36,7 @@ interface CollectionActionsProps {
|
|||||||
sg721Messages: SG721Instance | undefined
|
sg721Messages: SG721Instance | undefined
|
||||||
vendingMinterMessages: VendingMinterInstance | undefined
|
vendingMinterMessages: VendingMinterInstance | undefined
|
||||||
baseMinterMessages: BaseMinterInstance | undefined
|
baseMinterMessages: BaseMinterInstance | undefined
|
||||||
|
openEditionMinterMessages: OpenEditionMinterInstance | undefined
|
||||||
minterType: MinterType
|
minterType: MinterType
|
||||||
sg721Type: Sg721Type
|
sg721Type: Sg721Type
|
||||||
}
|
}
|
||||||
@ -47,6 +49,7 @@ export const CollectionActions = ({
|
|||||||
minterContractAddress,
|
minterContractAddress,
|
||||||
vendingMinterMessages,
|
vendingMinterMessages,
|
||||||
baseMinterMessages,
|
baseMinterMessages,
|
||||||
|
openEditionMinterMessages,
|
||||||
minterType,
|
minterType,
|
||||||
sg721Type,
|
sg721Type,
|
||||||
}: CollectionActionsProps) => {
|
}: CollectionActionsProps) => {
|
||||||
@ -54,6 +57,7 @@ export const CollectionActions = ({
|
|||||||
const [lastTx, setLastTx] = useState('')
|
const [lastTx, setLastTx] = useState('')
|
||||||
|
|
||||||
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
const [timestamp, setTimestamp] = useState<Date | undefined>(undefined)
|
||||||
|
const [endTimestamp, setEndTimestamp] = useState<Date | undefined>(undefined)
|
||||||
const [airdropAllocationArray, setAirdropAllocationArray] = useState<AirdropAllocation[]>([])
|
const [airdropAllocationArray, setAirdropAllocationArray] = useState<AirdropAllocation[]>([])
|
||||||
const [airdropArray, setAirdropArray] = useState<string[]>([])
|
const [airdropArray, setAirdropArray] = useState<string[]>([])
|
||||||
const [collectionInfo, setCollectionInfo] = useState<CollectionInfo>()
|
const [collectionInfo, setCollectionInfo] = useState<CollectionInfo>()
|
||||||
@ -168,6 +172,7 @@ export const CollectionActions = ({
|
|||||||
const showTokenUriField = isEitherType(type, ['mint_token_uri', 'update_token_metadata'])
|
const showTokenUriField = isEitherType(type, ['mint_token_uri', 'update_token_metadata'])
|
||||||
const showWhitelistField = type === 'set_whitelist'
|
const showWhitelistField = type === 'set_whitelist'
|
||||||
const showDateField = isEitherType(type, ['update_start_time', 'update_start_trading_time'])
|
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 showLimitField = type === 'update_per_address_limit'
|
||||||
const showTokenIdField = isEitherType(type, ['transfer', 'mint_for', 'burn', 'update_token_metadata'])
|
const showTokenIdField = isEitherType(type, ['transfer', 'mint_for', 'burn', 'update_token_metadata'])
|
||||||
const showNumberOfTokensField = type === 'batch_mint'
|
const showNumberOfTokensField = type === 'batch_mint'
|
||||||
@ -197,6 +202,7 @@ export const CollectionActions = ({
|
|||||||
const payload: DispatchExecuteArgs = {
|
const payload: DispatchExecuteArgs = {
|
||||||
whitelist: whitelistState.value,
|
whitelist: whitelistState.value,
|
||||||
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
|
startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '',
|
||||||
|
endTime: endTimestamp ? (endTimestamp.getTime() * 1_000_000).toString() : '',
|
||||||
limit: limitState.value,
|
limit: limitState.value,
|
||||||
minterContract: minterContractAddress,
|
minterContract: minterContractAddress,
|
||||||
sg721Contract: sg721ContractAddress,
|
sg721Contract: sg721ContractAddress,
|
||||||
@ -208,6 +214,7 @@ export const CollectionActions = ({
|
|||||||
batchNumber: batchNumberState.value,
|
batchNumber: batchNumberState.value,
|
||||||
vendingMinterMessages,
|
vendingMinterMessages,
|
||||||
baseMinterMessages,
|
baseMinterMessages,
|
||||||
|
openEditionMinterMessages,
|
||||||
sg721Messages,
|
sg721Messages,
|
||||||
recipient: resolvedRecipientAddress,
|
recipient: resolvedRecipientAddress,
|
||||||
recipients: airdropArray,
|
recipients: airdropArray,
|
||||||
@ -493,6 +500,11 @@ export const CollectionActions = ({
|
|||||||
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
|
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Conditional>
|
</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>
|
||||||
<div className="-mt-6">
|
<div className="-mt-6">
|
||||||
<div className="relative mb-2">
|
<div className="relative mb-2">
|
||||||
|
@ -6,9 +6,9 @@ import { Fragment, useEffect, useState } from 'react'
|
|||||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||||
|
|
||||||
import type { ActionListItem } from './actions'
|
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 type Sg721Type = 'updatable' | 'base'
|
||||||
|
|
||||||
export interface ActionsComboboxProps {
|
export interface ActionsComboboxProps {
|
||||||
@ -29,6 +29,9 @@ export const ActionsCombobox = ({ value, onChange, minterType, sg721Type }: Acti
|
|||||||
} else if (minterType === 'vending') {
|
} else if (minterType === 'vending') {
|
||||||
if (sg721Type === 'updatable') SET_ACTION_LIST(VENDING_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST))
|
if (sg721Type === 'updatable') SET_ACTION_LIST(VENDING_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST))
|
||||||
else SET_ACTION_LIST(VENDING_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))
|
} else SET_ACTION_LIST(VENDING_ACTION_LIST.concat(SG721_UPDATABLE_ACTION_LIST))
|
||||||
}, [minterType, sg721Type])
|
}, [minterType, sg721Type])
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable eslint-comments/disable-enable-pair */
|
/* eslint-disable eslint-comments/disable-enable-pair */
|
||||||
|
|
||||||
import { useBaseMinterContract } from 'contracts/baseMinter'
|
import { useBaseMinterContract } from 'contracts/baseMinter'
|
||||||
|
import { useOpenEditionMinterContract } from 'contracts/openEditionMinter'
|
||||||
import type { CollectionInfo, SG721Instance } from 'contracts/sg721'
|
import type { CollectionInfo, SG721Instance } from 'contracts/sg721'
|
||||||
import { useSG721Contract } from 'contracts/sg721'
|
import { useSG721Contract } from 'contracts/sg721'
|
||||||
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
||||||
@ -8,6 +8,7 @@ import { useVendingMinterContract } from 'contracts/vendingMinter'
|
|||||||
import type { AirdropAllocation } from 'utils/isValidAccountsFile'
|
import type { AirdropAllocation } from 'utils/isValidAccountsFile'
|
||||||
|
|
||||||
import type { BaseMinterInstance } from '../../../contracts/baseMinter/contract'
|
import type { BaseMinterInstance } from '../../../contracts/baseMinter/contract'
|
||||||
|
import type { OpenEditionMinterInstance } from '../../../contracts/openEditionMinter/contract'
|
||||||
|
|
||||||
export type ActionType = typeof ACTION_TYPES[number]
|
export type ActionType = typeof ACTION_TYPES[number]
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ export const ACTION_TYPES = [
|
|||||||
'batch_mint',
|
'batch_mint',
|
||||||
'set_whitelist',
|
'set_whitelist',
|
||||||
'update_start_time',
|
'update_start_time',
|
||||||
|
'update_end_time',
|
||||||
'update_start_trading_time',
|
'update_start_trading_time',
|
||||||
'update_per_address_limit',
|
'update_per_address_limit',
|
||||||
'update_collection_info',
|
'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[] = [
|
export const SG721_UPDATABLE_ACTION_LIST: ActionListItem[] = [
|
||||||
{
|
{
|
||||||
id: 'update_token_metadata',
|
id: 'update_token_metadata',
|
||||||
@ -231,6 +306,7 @@ export interface DispatchExecuteArgs {
|
|||||||
sg721Contract: string
|
sg721Contract: string
|
||||||
vendingMinterMessages?: VendingMinterInstance
|
vendingMinterMessages?: VendingMinterInstance
|
||||||
baseMinterMessages?: BaseMinterInstance
|
baseMinterMessages?: BaseMinterInstance
|
||||||
|
openEditionMinterMessages?: OpenEditionMinterInstance
|
||||||
sg721Messages?: SG721Instance
|
sg721Messages?: SG721Instance
|
||||||
txSigner: string
|
txSigner: string
|
||||||
type: string | undefined
|
type: string | undefined
|
||||||
@ -241,6 +317,7 @@ export interface DispatchExecuteArgs {
|
|||||||
batchNumber: number
|
batchNumber: number
|
||||||
whitelist: string
|
whitelist: string
|
||||||
startTime: string | undefined
|
startTime: string | undefined
|
||||||
|
endTime: string | undefined
|
||||||
limit: number
|
limit: number
|
||||||
tokenIds: string
|
tokenIds: string
|
||||||
recipients: string[]
|
recipients: string[]
|
||||||
@ -250,8 +327,8 @@ export interface DispatchExecuteArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||||
const { vendingMinterMessages, baseMinterMessages, sg721Messages, txSigner } = args
|
const { vendingMinterMessages, baseMinterMessages, openEditionMinterMessages, sg721Messages, txSigner } = args
|
||||||
if (!vendingMinterMessages || !baseMinterMessages || !sg721Messages) {
|
if (!vendingMinterMessages || !baseMinterMessages || !openEditionMinterMessages || !sg721Messages) {
|
||||||
throw new Error('Cannot execute actions')
|
throw new Error('Cannot execute actions')
|
||||||
}
|
}
|
||||||
switch (args.type) {
|
switch (args.type) {
|
||||||
@ -282,6 +359,9 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
|||||||
case 'update_start_time': {
|
case 'update_start_time': {
|
||||||
return vendingMinterMessages.updateStartTime(txSigner, args.startTime as string)
|
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': {
|
case 'update_start_trading_time': {
|
||||||
return vendingMinterMessages.updateStartTradingTime(txSigner, args.startTime)
|
return vendingMinterMessages.updateStartTradingTime(txSigner, args.startTime)
|
||||||
}
|
}
|
||||||
@ -346,6 +426,8 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
|||||||
const { messages: sg721Messages } = useSG721Contract()
|
const { messages: sg721Messages } = useSG721Contract()
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
const { messages: baseMinterMessages } = useBaseMinterContract()
|
const { messages: baseMinterMessages } = useBaseMinterContract()
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const { messages: openEditionMinterMessages } = useOpenEditionMinterContract()
|
||||||
const { minterContract, sg721Contract } = args
|
const { minterContract, sg721Contract } = args
|
||||||
switch (args.type) {
|
switch (args.type) {
|
||||||
case 'mint_token_uri': {
|
case 'mint_token_uri': {
|
||||||
@ -375,6 +457,9 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
|||||||
case 'update_start_time': {
|
case 'update_start_time': {
|
||||||
return vendingMinterMessages(minterContract)?.updateStartTime(args.startTime as string)
|
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': {
|
case 'update_start_trading_time': {
|
||||||
return vendingMinterMessages(minterContract)?.updateStartTradingTime(args.startTime as string)
|
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 { MinterType } from '../actions/Combobox'
|
||||||
import type { QueryListItem } from './query'
|
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 {
|
export interface QueryComboboxProps {
|
||||||
value: QueryListItem | null
|
value: QueryListItem | null
|
||||||
@ -22,6 +22,8 @@ export const QueryCombobox = ({ value, onChange, minterType }: QueryComboboxProp
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (minterType === 'base') {
|
if (minterType === 'base') {
|
||||||
SET_QUERY_LIST(BASE_QUERY_LIST)
|
SET_QUERY_LIST(BASE_QUERY_LIST)
|
||||||
|
} else if (minterType === 'openEdition') {
|
||||||
|
SET_QUERY_LIST(OPEN_EDITION_QUERY_LIST)
|
||||||
} else {
|
} else {
|
||||||
SET_QUERY_LIST(VENDING_QUERY_LIST)
|
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 { useInputState } from 'components/forms/FormInput.hooks'
|
||||||
import { JsonPreview } from 'components/JsonPreview'
|
import { JsonPreview } from 'components/JsonPreview'
|
||||||
import type { BaseMinterInstance } from 'contracts/baseMinter'
|
import type { BaseMinterInstance } from 'contracts/baseMinter'
|
||||||
|
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter'
|
||||||
import type { SG721Instance } from 'contracts/sg721'
|
import type { SG721Instance } from 'contracts/sg721'
|
||||||
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
@ -21,6 +22,7 @@ interface CollectionQueriesProps {
|
|||||||
sg721Messages: SG721Instance | undefined
|
sg721Messages: SG721Instance | undefined
|
||||||
vendingMinterMessages: VendingMinterInstance | undefined
|
vendingMinterMessages: VendingMinterInstance | undefined
|
||||||
baseMinterMessages: BaseMinterInstance | undefined
|
baseMinterMessages: BaseMinterInstance | undefined
|
||||||
|
openEditionMinterMessages: OpenEditionMinterInstance | undefined
|
||||||
minterType: MinterType
|
minterType: MinterType
|
||||||
}
|
}
|
||||||
export const CollectionQueries = ({
|
export const CollectionQueries = ({
|
||||||
@ -28,6 +30,7 @@ export const CollectionQueries = ({
|
|||||||
sg721Messages,
|
sg721Messages,
|
||||||
minterContractAddress,
|
minterContractAddress,
|
||||||
vendingMinterMessages,
|
vendingMinterMessages,
|
||||||
|
openEditionMinterMessages,
|
||||||
baseMinterMessages,
|
baseMinterMessages,
|
||||||
minterType,
|
minterType,
|
||||||
}: CollectionQueriesProps) => {
|
}: CollectionQueriesProps) => {
|
||||||
@ -57,9 +60,25 @@ export const CollectionQueries = ({
|
|||||||
const showAddressField = type === 'tokens_minted_to_user' || type === 'tokens'
|
const showAddressField = type === 'tokens_minted_to_user' || type === 'tokens'
|
||||||
|
|
||||||
const { data: response } = useQuery(
|
const { data: response } = useQuery(
|
||||||
[sg721Messages, baseMinterMessages, vendingMinterMessages, type, tokenId, address] as const,
|
[
|
||||||
|
sg721Messages,
|
||||||
|
baseMinterMessages,
|
||||||
|
vendingMinterMessages,
|
||||||
|
openEditionMinterMessages,
|
||||||
|
type,
|
||||||
|
tokenId,
|
||||||
|
address,
|
||||||
|
] as const,
|
||||||
async ({ queryKey }) => {
|
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
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const res = await resolveAddress(_address, wallet).then(async (resolvedAddress) => {
|
const res = await resolveAddress(_address, wallet).then(async (resolvedAddress) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
@ -67,6 +86,7 @@ export const CollectionQueries = ({
|
|||||||
tokenId: _tokenId,
|
tokenId: _tokenId,
|
||||||
vendingMinterMessages: _vendingMinterMessages,
|
vendingMinterMessages: _vendingMinterMessages,
|
||||||
baseMinterMessages: _baseMinterMessages_,
|
baseMinterMessages: _baseMinterMessages_,
|
||||||
|
openEditionMinterMessages: _openEditionMinterMessages,
|
||||||
sg721Messages: _sg721Messages,
|
sg721Messages: _sg721Messages,
|
||||||
address: resolvedAddress,
|
address: resolvedAddress,
|
||||||
type: _type,
|
type: _type,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { BaseMinterInstance } from 'contracts/baseMinter'
|
import type { BaseMinterInstance } from 'contracts/baseMinter'
|
||||||
|
import type { OpenEditionMinterInstance } from 'contracts/openEditionMinter/contract'
|
||||||
import type { SG721Instance } from 'contracts/sg721'
|
import type { SG721Instance } from 'contracts/sg721'
|
||||||
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
||||||
|
|
||||||
@ -9,6 +10,7 @@ export const QUERY_TYPES = [
|
|||||||
'mint_price',
|
'mint_price',
|
||||||
'num_tokens',
|
'num_tokens',
|
||||||
'tokens_minted_to_user',
|
'tokens_minted_to_user',
|
||||||
|
'total_mint_count',
|
||||||
'tokens',
|
'tokens',
|
||||||
// 'token_owners',
|
// 'token_owners',
|
||||||
'token_info',
|
'token_info',
|
||||||
@ -91,6 +93,48 @@ export const BASE_QUERY_LIST: QueryListItem[] = [
|
|||||||
description: `Query Minter Status`,
|
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 {
|
export interface DispatchExecuteProps {
|
||||||
type: QueryType
|
type: QueryType
|
||||||
@ -102,6 +146,7 @@ type Select<T extends QueryType> = T
|
|||||||
export type DispatchQueryArgs = {
|
export type DispatchQueryArgs = {
|
||||||
baseMinterMessages?: BaseMinterInstance
|
baseMinterMessages?: BaseMinterInstance
|
||||||
vendingMinterMessages?: VendingMinterInstance
|
vendingMinterMessages?: VendingMinterInstance
|
||||||
|
openEditionMinterMessages?: OpenEditionMinterInstance
|
||||||
sg721Messages?: SG721Instance
|
sg721Messages?: SG721Instance
|
||||||
} & (
|
} & (
|
||||||
| { type: undefined }
|
| { type: undefined }
|
||||||
@ -109,6 +154,7 @@ export type DispatchQueryArgs = {
|
|||||||
| { type: Select<'mint_price'> }
|
| { type: Select<'mint_price'> }
|
||||||
| { type: Select<'num_tokens'> }
|
| { type: Select<'num_tokens'> }
|
||||||
| { type: Select<'tokens_minted_to_user'>; address: string }
|
| { type: Select<'tokens_minted_to_user'>; address: string }
|
||||||
|
| { type: Select<'total_mint_count'> }
|
||||||
| { type: Select<'tokens'>; address: string }
|
| { type: Select<'tokens'>; address: string }
|
||||||
// | { type: Select<'token_owners'> }
|
// | { type: Select<'token_owners'> }
|
||||||
| { type: Select<'token_info'>; tokenId: string }
|
| { type: Select<'token_info'>; tokenId: string }
|
||||||
@ -117,8 +163,8 @@ export type DispatchQueryArgs = {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const dispatchQuery = async (args: DispatchQueryArgs) => {
|
export const dispatchQuery = async (args: DispatchQueryArgs) => {
|
||||||
const { baseMinterMessages, vendingMinterMessages, sg721Messages } = args
|
const { baseMinterMessages, vendingMinterMessages, openEditionMinterMessages, sg721Messages } = args
|
||||||
if (!baseMinterMessages || !vendingMinterMessages || !sg721Messages) {
|
if (!baseMinterMessages || !vendingMinterMessages || !openEditionMinterMessages || !sg721Messages) {
|
||||||
throw new Error('Cannot execute actions')
|
throw new Error('Cannot execute actions')
|
||||||
}
|
}
|
||||||
switch (args.type) {
|
switch (args.type) {
|
||||||
@ -134,6 +180,9 @@ export const dispatchQuery = async (args: DispatchQueryArgs) => {
|
|||||||
case 'tokens_minted_to_user': {
|
case 'tokens_minted_to_user': {
|
||||||
return vendingMinterMessages.getMintCount(args.address)
|
return vendingMinterMessages.getMintCount(args.address)
|
||||||
}
|
}
|
||||||
|
case 'total_mint_count': {
|
||||||
|
return openEditionMinterMessages.getTotalMintCount()
|
||||||
|
}
|
||||||
case 'tokens': {
|
case 'tokens': {
|
||||||
return sg721Messages.tokens(args.address)
|
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 { useBaseFactoryContract } from 'contracts/baseFactory'
|
||||||
import type { UseBaseMinterContractProps } from 'contracts/baseMinter'
|
import type { UseBaseMinterContractProps } from 'contracts/baseMinter'
|
||||||
import { useBaseMinterContract } 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 type { UseSG721ContractProps } from 'contracts/sg721'
|
||||||
import { useSG721Contract } from 'contracts/sg721'
|
import { useSG721Contract } from 'contracts/sg721'
|
||||||
import type { UseVendingFactoryContractProps } from 'contracts/vendingFactory'
|
import type { UseVendingFactoryContractProps } from 'contracts/vendingFactory'
|
||||||
@ -27,9 +29,11 @@ export interface ContractsStore extends State {
|
|||||||
sg721: UseSG721ContractProps | null
|
sg721: UseSG721ContractProps | null
|
||||||
vendingMinter: UseVendingMinterContractProps | null
|
vendingMinter: UseVendingMinterContractProps | null
|
||||||
baseMinter: UseBaseMinterContractProps | null
|
baseMinter: UseBaseMinterContractProps | null
|
||||||
|
openEditionMinter: UseOpenEditionMinterContractProps | null
|
||||||
whitelist: UseWhiteListContractProps | null
|
whitelist: UseWhiteListContractProps | null
|
||||||
vendingFactory: UseVendingFactoryContractProps | null
|
vendingFactory: UseVendingFactoryContractProps | null
|
||||||
baseFactory: UseBaseFactoryContractProps | null
|
baseFactory: UseBaseFactoryContractProps | null
|
||||||
|
openEditionFactory: UseOpenEditionFactoryContractProps | null
|
||||||
badgeHub: UseBadgeHubContractProps | null
|
badgeHub: UseBadgeHubContractProps | null
|
||||||
splits: UseSplitsContractProps | null
|
splits: UseSplitsContractProps | null
|
||||||
}
|
}
|
||||||
@ -41,9 +45,11 @@ export const defaultValues: ContractsStore = {
|
|||||||
sg721: null,
|
sg721: null,
|
||||||
vendingMinter: null,
|
vendingMinter: null,
|
||||||
baseMinter: null,
|
baseMinter: null,
|
||||||
|
openEditionMinter: null,
|
||||||
whitelist: null,
|
whitelist: null,
|
||||||
vendingFactory: null,
|
vendingFactory: null,
|
||||||
baseFactory: null,
|
baseFactory: null,
|
||||||
|
openEditionFactory: null,
|
||||||
badgeHub: null,
|
badgeHub: null,
|
||||||
splits: null,
|
splits: null,
|
||||||
}
|
}
|
||||||
@ -72,9 +78,11 @@ const ContractsSubscription: VFC = () => {
|
|||||||
const sg721 = useSG721Contract()
|
const sg721 = useSG721Contract()
|
||||||
const vendingMinter = useVendingMinterContract()
|
const vendingMinter = useVendingMinterContract()
|
||||||
const baseMinter = useBaseMinterContract()
|
const baseMinter = useBaseMinterContract()
|
||||||
|
const openEditionMinter = useOpenEditionMinterContract()
|
||||||
const whitelist = useWhiteListContract()
|
const whitelist = useWhiteListContract()
|
||||||
const vendingFactory = useVendingFactoryContract()
|
const vendingFactory = useVendingFactoryContract()
|
||||||
const baseFactory = useBaseFactoryContract()
|
const baseFactory = useBaseFactoryContract()
|
||||||
|
const openEditionFactory = useOpenEditionFactoryContract()
|
||||||
const badgeHub = useBadgeHubContract()
|
const badgeHub = useBadgeHubContract()
|
||||||
const splits = useSplitsContract()
|
const splits = useSplitsContract()
|
||||||
|
|
||||||
@ -83,9 +91,11 @@ const ContractsSubscription: VFC = () => {
|
|||||||
sg721,
|
sg721,
|
||||||
vendingMinter,
|
vendingMinter,
|
||||||
baseMinter,
|
baseMinter,
|
||||||
|
openEditionMinter,
|
||||||
whitelist,
|
whitelist,
|
||||||
vendingFactory,
|
vendingFactory,
|
||||||
baseFactory,
|
baseFactory,
|
||||||
|
openEditionFactory,
|
||||||
badgeHub,
|
badgeHub,
|
||||||
splits,
|
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_CODE_ID: string
|
||||||
readonly NEXT_PUBLIC_VENDING_MINTER_FLEX_CODE_ID: string
|
readonly NEXT_PUBLIC_VENDING_MINTER_FLEX_CODE_ID: string
|
||||||
readonly NEXT_PUBLIC_VENDING_FACTORY_ADDRESS: 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_UPDATABLE_ADDRESS: string
|
||||||
readonly NEXT_PUBLIC_VENDING_FACTORY_FLEX_ADDRESS: string
|
readonly NEXT_PUBLIC_VENDING_FACTORY_FLEX_ADDRESS: string
|
||||||
readonly NEXT_PUBLIC_BASE_FACTORY_ADDRESS: string
|
readonly NEXT_PUBLIC_BASE_FACTORY_ADDRESS: string
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "stargaze-studio",
|
"name": "stargaze-studio",
|
||||||
"version": "0.6.4",
|
"version": "0.6.5",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
|
@ -19,7 +19,12 @@ import { links } from 'utils/links'
|
|||||||
import type { MinterType, Sg721Type } from '../../components/collections/actions/Combobox'
|
import type { MinterType, Sg721Type } from '../../components/collections/actions/Combobox'
|
||||||
|
|
||||||
const CollectionActionsPage: NextPage = () => {
|
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 wallet = useWallet()
|
||||||
|
|
||||||
const [action, setAction] = useState<boolean>(false)
|
const [action, setAction] = useState<boolean>(false)
|
||||||
@ -51,6 +56,11 @@ const CollectionActionsPage: NextPage = () => {
|
|||||||
() => baseMinterContract?.use(minterContractState.value),
|
() => baseMinterContract?.use(minterContractState.value),
|
||||||
[baseMinterContract, minterContractState.value],
|
[baseMinterContract, minterContractState.value],
|
||||||
)
|
)
|
||||||
|
const openEditionMinterMessages = useMemo(
|
||||||
|
() => openEditionMinterContract?.use(minterContractState.value),
|
||||||
|
[openEditionMinterContract, minterContractState.value],
|
||||||
|
)
|
||||||
|
|
||||||
const sg721Messages = useMemo(
|
const sg721Messages = useMemo(
|
||||||
() => sg721Contract?.use(sg721ContractState.value),
|
() => sg721Contract?.use(sg721ContractState.value),
|
||||||
[sg721Contract, sg721ContractState.value],
|
[sg721Contract, sg721ContractState.value],
|
||||||
@ -105,6 +115,8 @@ const CollectionActionsPage: NextPage = () => {
|
|||||||
.then((contract) => {
|
.then((contract) => {
|
||||||
if (contract?.includes('sg-base-minter')) {
|
if (contract?.includes('sg-base-minter')) {
|
||||||
setMinterType('base')
|
setMinterType('base')
|
||||||
|
} else if (contract?.includes('open-edition')) {
|
||||||
|
setMinterType('openEdition')
|
||||||
} else {
|
} else {
|
||||||
setMinterType('vending')
|
setMinterType('vending')
|
||||||
}
|
}
|
||||||
@ -214,6 +226,7 @@ const CollectionActionsPage: NextPage = () => {
|
|||||||
baseMinterMessages={baseMinterMessages}
|
baseMinterMessages={baseMinterMessages}
|
||||||
minterContractAddress={minterContractState.value}
|
minterContractAddress={minterContractState.value}
|
||||||
minterType={minterType}
|
minterType={minterType}
|
||||||
|
openEditionMinterMessages={openEditionMinterMessages}
|
||||||
sg721ContractAddress={sg721ContractState.value}
|
sg721ContractAddress={sg721ContractState.value}
|
||||||
sg721Messages={sg721Messages}
|
sg721Messages={sg721Messages}
|
||||||
sg721Type={sg721Type}
|
sg721Type={sg721Type}
|
||||||
@ -224,6 +237,7 @@ const CollectionActionsPage: NextPage = () => {
|
|||||||
baseMinterMessages={baseMinterMessages}
|
baseMinterMessages={baseMinterMessages}
|
||||||
minterContractAddress={minterContractState.value}
|
minterContractAddress={minterContractState.value}
|
||||||
minterType={minterType}
|
minterType={minterType}
|
||||||
|
openEditionMinterMessages={openEditionMinterMessages}
|
||||||
sg721ContractAddress={sg721ContractState.value}
|
sg721ContractAddress={sg721ContractState.value}
|
||||||
sg721Messages={sg721Messages}
|
sg721Messages={sg721Messages}
|
||||||
vendingMinterMessages={vendingMinterMessages}
|
vendingMinterMessages={vendingMinterMessages}
|
||||||
|
@ -30,6 +30,8 @@ import type { UploadDetailsDataProps } from 'components/collections/creation/Upl
|
|||||||
import type { WhitelistDetailsDataProps } from 'components/collections/creation/WhitelistDetails'
|
import type { WhitelistDetailsDataProps } from 'components/collections/creation/WhitelistDetails'
|
||||||
import { Conditional } from 'components/Conditional'
|
import { Conditional } from 'components/Conditional'
|
||||||
import { LoadingModal } from 'components/LoadingModal'
|
import { LoadingModal } from 'components/LoadingModal'
|
||||||
|
import type { OpenEditionMinterCreatorDataProps } from 'components/openEdition/OpenEditionMinterCreator'
|
||||||
|
import { OpenEditionMinterCreator } from 'components/openEdition/OpenEditionMinterCreator'
|
||||||
import { useContracts } from 'contexts/contracts'
|
import { useContracts } from 'contexts/contracts'
|
||||||
import { addLogItem } from 'contexts/log'
|
import { addLogItem } from 'contexts/log'
|
||||||
import { useWallet } from 'contexts/wallet'
|
import { useWallet } from 'contexts/wallet'
|
||||||
@ -48,6 +50,8 @@ import {
|
|||||||
BASE_FACTORY_UPDATABLE_ADDRESS,
|
BASE_FACTORY_UPDATABLE_ADDRESS,
|
||||||
BLOCK_EXPLORER_URL,
|
BLOCK_EXPLORER_URL,
|
||||||
NETWORK,
|
NETWORK,
|
||||||
|
OPEN_EDITION_FACTORY_ADDRESS,
|
||||||
|
OPEN_EDITION_UPDATABLE_FACTORY_ADDRESS,
|
||||||
SG721_CODE_ID,
|
SG721_CODE_ID,
|
||||||
SG721_UPDATABLE_CODE_ID,
|
SG721_UPDATABLE_CODE_ID,
|
||||||
STARGAZE_URL,
|
STARGAZE_URL,
|
||||||
@ -91,6 +95,9 @@ const CollectionCreationPage: NextPage = () => {
|
|||||||
const [uploadDetails, setUploadDetails] = useState<UploadDetailsDataProps | null>(null)
|
const [uploadDetails, setUploadDetails] = useState<UploadDetailsDataProps | null>(null)
|
||||||
const [collectionDetails, setCollectionDetails] = useState<CollectionDetailsDataProps | null>(null)
|
const [collectionDetails, setCollectionDetails] = useState<CollectionDetailsDataProps | null>(null)
|
||||||
const [baseMinterDetails, setBaseMinterDetails] = useState<BaseMinterDetailsDataProps | null>(null)
|
const [baseMinterDetails, setBaseMinterDetails] = useState<BaseMinterDetailsDataProps | null>(null)
|
||||||
|
const [openEditionMinterDetails, setOpenEditionMinterDetails] = useState<OpenEditionMinterCreatorDataProps | null>(
|
||||||
|
null,
|
||||||
|
)
|
||||||
const [mintingDetails, setMintingDetails] = useState<MintingDetailsDataProps | null>(null)
|
const [mintingDetails, setMintingDetails] = useState<MintingDetailsDataProps | null>(null)
|
||||||
const [whitelistDetails, setWhitelistDetails] = useState<WhitelistDetailsDataProps | null>(null)
|
const [whitelistDetails, setWhitelistDetails] = useState<WhitelistDetailsDataProps | null>(null)
|
||||||
const [royaltyDetails, setRoyaltyDetails] = useState<RoyaltyDetailsDataProps | null>(null)
|
const [royaltyDetails, setRoyaltyDetails] = useState<RoyaltyDetailsDataProps | null>(null)
|
||||||
@ -99,10 +106,16 @@ const CollectionCreationPage: NextPage = () => {
|
|||||||
const [vendingMinterCreationFee, setVendingMinterCreationFee] = useState<string | null>(null)
|
const [vendingMinterCreationFee, setVendingMinterCreationFee] = useState<string | null>(null)
|
||||||
const [baseMinterCreationFee, setBaseMinterCreationFee] = useState<string | null>(null)
|
const [baseMinterCreationFee, setBaseMinterCreationFee] = useState<string | null>(null)
|
||||||
const [vendingMinterUpdatableCreationFee, setVendingMinterUpdatableCreationFee] = 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 [vendingMinterFlexCreationFee, setVendingMinterFlexCreationFee] = useState<string | null>(null)
|
||||||
const [baseMinterUpdatableCreationFee, setBaseMinterUpdatableCreationFee] = useState<string | null>(null)
|
const [baseMinterUpdatableCreationFee, setBaseMinterUpdatableCreationFee] = useState<string | null>(null)
|
||||||
const [minimumMintPrice, setMinimumMintPrice] = useState<string | null>('0')
|
const [minimumMintPrice, setMinimumMintPrice] = useState<string | null>('0')
|
||||||
const [minimumUpdatableMintPrice, setMinimumUpdatableMintPrice] = 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 [minimumFlexMintPrice, setMinimumFlexMintPrice] = useState<string | null>('0')
|
||||||
|
|
||||||
const [uploading, setUploading] = useState(false)
|
const [uploading, setUploading] = useState(false)
|
||||||
@ -1031,6 +1044,26 @@ const CollectionCreationPage: NextPage = () => {
|
|||||||
setVendingMinterFlexCreationFee(vendingFactoryFlexParameters?.params?.creation_fee?.amount)
|
setVendingMinterFlexCreationFee(vendingFactoryFlexParameters?.params?.creation_fee?.amount)
|
||||||
setMinimumFlexMintPrice(vendingFactoryFlexParameters?.params?.min_mint_price?.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 = () => {
|
const checkwalletBalance = () => {
|
||||||
@ -1069,9 +1102,13 @@ const CollectionCreationPage: NextPage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (vendingMinterContractAddress !== null || isMintingComplete)
|
if (
|
||||||
|
vendingMinterContractAddress !== null ||
|
||||||
|
openEditionMinterDetails?.openEditionMinterContractAddress ||
|
||||||
|
isMintingComplete
|
||||||
|
)
|
||||||
scrollRef.current?.scrollIntoView({ behavior: 'smooth' })
|
scrollRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||||
}, [vendingMinterContractAddress, isMintingComplete])
|
}, [vendingMinterContractAddress, openEditionMinterDetails?.openEditionMinterContractAddress, isMintingComplete])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBaseTokenUri(uploadDetails?.baseTokenURI as string)
|
setBaseTokenUri(uploadDetails?.baseTokenURI as string)
|
||||||
@ -1118,6 +1155,67 @@ const CollectionCreationPage: NextPage = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-10" ref={scrollRef}>
|
<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}>
|
<Conditional test={vendingMinterContractAddress !== null || isMintingComplete}>
|
||||||
<Alert className="mt-5" type="info">
|
<Alert className="mt-5" type="info">
|
||||||
<div>
|
<div>
|
||||||
@ -1289,80 +1387,108 @@ const CollectionCreationPage: NextPage = () => {
|
|||||||
</Conditional>
|
</Conditional>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* To be removed */}
|
<div>
|
||||||
<Conditional test={BASE_FACTORY_ADDRESS === undefined}>
|
<div
|
||||||
<div className="mx-10 mt-5" />
|
className={clsx(
|
||||||
</Conditional>
|
'mx-10 mt-5',
|
||||||
<Conditional test={BASE_FACTORY_ADDRESS !== undefined}>
|
'grid before:absolute relative grid-cols-3 grid-flow-col items-stretch rounded',
|
||||||
{/* /To be removed */}
|
'before:inset-x-0 before:bottom-0 before:border-white/25',
|
||||||
<div>
|
)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'mx-10 mt-5',
|
'isolate space-y-1 border-2',
|
||||||
'grid before:absolute relative grid-cols-2 grid-flow-col items-stretch rounded',
|
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
||||||
'before:inset-x-0 before:bottom-0 before:border-white/25',
|
minterType === 'vending' ? 'border-stargaze' : 'border-transparent',
|
||||||
|
minterType !== 'vending' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<button
|
||||||
className={clsx(
|
className="p-4 w-full h-full text-left bg-transparent"
|
||||||
'isolate space-y-1 border-2',
|
onClick={() => {
|
||||||
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
setMinterType('vending')
|
||||||
minterType === 'vending' ? 'border-stargaze' : 'border-transparent',
|
resetReadyFlags()
|
||||||
minterType !== 'vending' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
}}
|
||||||
)}
|
type="button"
|
||||||
>
|
>
|
||||||
<button
|
<h4 className="font-bold">Standard Collection</h4>
|
||||||
className="p-4 w-full h-full text-left bg-transparent"
|
<span className="text-sm text-white/80 line-clamp-2">
|
||||||
onClick={() => {
|
A non-appendable collection that facilitates primary market vending machine style minting
|
||||||
setMinterType('vending')
|
</span>
|
||||||
resetReadyFlags()
|
</button>
|
||||||
}}
|
</div>
|
||||||
type="button"
|
<div
|
||||||
>
|
className={clsx(
|
||||||
<h4 className="font-bold">Standard Collection</h4>
|
'isolate space-y-1 border-2',
|
||||||
<span className="text-sm text-white/80 line-clamp-2">
|
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
||||||
A non-appendable collection that facilitates primary market vending machine style minting
|
minterType === 'base' ? 'border-stargaze' : 'border-transparent',
|
||||||
</span>
|
minterType !== 'base' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
||||||
</button>
|
)}
|
||||||
</div>
|
>
|
||||||
<div
|
<button
|
||||||
className={clsx(
|
className="p-4 w-full h-full text-left bg-transparent"
|
||||||
'isolate space-y-1 border-2',
|
onClick={() => {
|
||||||
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
setMinterType('base')
|
||||||
minterType === 'base' ? 'border-stargaze' : 'border-transparent',
|
resetReadyFlags()
|
||||||
minterType !== 'base' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
}}
|
||||||
)}
|
type="button"
|
||||||
>
|
>
|
||||||
<button
|
<h4 className="font-bold">1/1 Collection</h4>
|
||||||
className="p-4 w-full h-full text-left bg-transparent"
|
<span className="text-sm text-white/80 line-clamp-2">
|
||||||
onClick={() => {
|
An appendable collection that only allows for direct secondary market listing of tokens
|
||||||
setMinterType('base')
|
</span>
|
||||||
resetReadyFlags()
|
</button>
|
||||||
}}
|
</div>
|
||||||
type="button"
|
<div
|
||||||
>
|
className={clsx(
|
||||||
<h4 className="font-bold">1/1 Collection</h4>
|
'isolate space-y-1 border-2',
|
||||||
<span className="text-sm text-white/80 line-clamp-2">
|
'first-of-type:rounded-tl-md last-of-type:rounded-tr-md',
|
||||||
An appendable collection that only allows for direct secondary market listing of tokens
|
minterType === 'openEdition' ? 'border-stargaze' : 'border-transparent',
|
||||||
</span>
|
minterType !== 'openEdition' ? 'bg-stargaze/5 hover:bg-stargaze/80' : 'hover:bg-white/5',
|
||||||
</button>
|
OPEN_EDITION_FACTORY_ADDRESS === undefined ? 'hover:bg-zinc-500 opacity-50 hover:opacity-70' : '',
|
||||||
</div>
|
)}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Conditional>
|
</div>
|
||||||
|
|
||||||
{minterType === 'base' && (
|
{minterType === 'base' && (
|
||||||
<div>
|
<div>
|
||||||
<BaseMinterDetails minterType={minterType} onChange={setBaseMinterDetails} />
|
<BaseMinterDetails minterType={minterType} onChange={setBaseMinterDetails} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<Conditional test={minterType === 'openEdition'}>
|
||||||
<div className="mx-10">
|
<OpenEditionMinterCreator
|
||||||
<UploadDetails
|
minimumMintPrice={minimumOpenEditionMintPrice as string}
|
||||||
baseMinterAcquisitionMethod={baseMinterDetails?.baseMinterAcquisitionMethod}
|
minimumUpdatableMintPrice={minimumOpenEditionUpdatableMintPrice as string}
|
||||||
minterType={minterType}
|
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
|
<Conditional
|
||||||
test={
|
test={
|
||||||
|
@ -27,6 +27,7 @@ const CollectionList: NextPage = () => {
|
|||||||
const [myCollections, setMyCollections] = useState<any[]>([])
|
const [myCollections, setMyCollections] = useState<any[]>([])
|
||||||
const [myOneOfOneCollections, setMyOneOfOneCollections] = useState<any[]>([])
|
const [myOneOfOneCollections, setMyOneOfOneCollections] = useState<any[]>([])
|
||||||
const [myStandardCollections, setMyStandardCollections] = useState<any[]>([])
|
const [myStandardCollections, setMyStandardCollections] = useState<any[]>([])
|
||||||
|
const [myOpenEditionCollections, setMyOpenEditionCollections] = useState<any[]>([])
|
||||||
|
|
||||||
async function getMinterContractType(minterContractAddress: string) {
|
async function getMinterContractType(minterContractAddress: string) {
|
||||||
if (wallet.client && minterContractAddress.length > 0) {
|
if (wallet.client && minterContractAddress.length > 0) {
|
||||||
@ -43,6 +44,7 @@ const CollectionList: NextPage = () => {
|
|||||||
const filterMyCollections = () => {
|
const filterMyCollections = () => {
|
||||||
setMyOneOfOneCollections([])
|
setMyOneOfOneCollections([])
|
||||||
setMyStandardCollections([])
|
setMyStandardCollections([])
|
||||||
|
setMyOpenEditionCollections([])
|
||||||
if (myCollections.length > 0) {
|
if (myCollections.length > 0) {
|
||||||
myCollections.map(async (collection: any) => {
|
myCollections.map(async (collection: any) => {
|
||||||
await getMinterContractType(collection.minter)
|
await getMinterContractType(collection.minter)
|
||||||
@ -51,6 +53,8 @@ const CollectionList: NextPage = () => {
|
|||||||
setMyOneOfOneCollections((prevState) => [...prevState, collection])
|
setMyOneOfOneCollections((prevState) => [...prevState, collection])
|
||||||
} else if (contractType?.includes('sg-minter') || contractType?.includes('flex')) {
|
} else if (contractType?.includes('sg-minter') || contractType?.includes('flex')) {
|
||||||
setMyStandardCollections((prevState) => [...prevState, collection])
|
setMyStandardCollections((prevState) => [...prevState, collection])
|
||||||
|
} else if (contractType?.includes('open-edition')) {
|
||||||
|
setMyOpenEditionCollections((prevState) => [...prevState, collection])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -302,11 +306,120 @@ const CollectionList: NextPage = () => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}, [myCollections, myStandardCollections, myOneOfOneCollections, wallet.address])
|
}, [myCollections, myStandardCollections, myOneOfOneCollections, myOpenEditionCollections, wallet.address])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-6 px-12 space-y-4">
|
<section className="py-6 px-12 space-y-4">
|
||||||
|
@ -100,7 +100,7 @@ const BaseMinterQueryPage: NextPage = () => {
|
|||||||
onChange={(e) => setType(e.target.value as QueryType)}
|
onChange={(e) => setType(e.target.value as QueryType)}
|
||||||
>
|
>
|
||||||
{QUERY_LIST.map(({ id, name }) => (
|
{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}
|
{name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
|
@ -4,7 +4,7 @@ import type { NextPage } from 'next'
|
|||||||
// import Brand from 'public/brand/brand.svg'
|
// import Brand from 'public/brand/brand.svg'
|
||||||
import { withMetadata } from 'utils/layout'
|
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 = () => {
|
const HomePage: NextPage = () => {
|
||||||
return (
|
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">
|
<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.
|
Execute messages and run queries on Stargaze's SG721 contract.
|
||||||
</HomeCard>
|
</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
|
<HomeCard
|
||||||
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
|
className="p-4 -m-4 hover:bg-gray-500/10 rounded"
|
||||||
link="/contracts/whitelist"
|
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)}
|
onChange={(e) => setType(e.target.value as QueryType)}
|
||||||
>
|
>
|
||||||
{QUERY_LIST.map(({ id, name }) => (
|
{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}
|
{name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
|
@ -105,7 +105,7 @@ const VendingMinterQueryPage: NextPage = () => {
|
|||||||
onChange={(e) => setType(e.target.value as QueryType)}
|
onChange={(e) => setType(e.target.value as QueryType)}
|
||||||
>
|
>
|
||||||
{QUERY_LIST.map(({ id, name }) => (
|
{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}
|
{name}
|
||||||
</option>
|
</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 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_ADDRESS = process.env.NEXT_PUBLIC_BASE_FACTORY_ADDRESS
|
||||||
export const BASE_FACTORY_UPDATABLE_ADDRESS = process.env.NEXT_PUBLIC_BASE_FACTORY_UPDATABLE_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 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 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)
|
export const BADGE_HUB_CODE_ID = parseInt(process.env.NEXT_PUBLIC_BADGE_HUB_CODE_ID, 10)
|
||||||
|
Loading…
Reference in New Issue
Block a user