Implement Collection Actions changes for Base Minter contract

This commit is contained in:
Serkan Reis 2022-12-09 21:47:03 +03:00
parent a8c2548554
commit c9d4734417
9 changed files with 249 additions and 30 deletions

View File

@ -13,6 +13,7 @@ import { InputDateTime } from 'components/InputDateTime'
import { JsonPreview } from 'components/JsonPreview'
import { TransactionHash } from 'components/TransactionHash'
import { useWallet } from 'contexts/wallet'
import type { BaseMinterInstance } from 'contracts/baseMinter'
import type { SG721Instance } from 'contracts/sg721'
import type { VendingMinterInstance } from 'contracts/vendingMinter'
import type { FormEvent } from 'react'
@ -24,12 +25,15 @@ import type { AirdropAllocation } from 'utils/isValidAccountsFile'
import type { CollectionInfo } from '../../../contracts/sg721/contract'
import { TextInput } from '../../forms/FormInput'
import type { MinterType } from './Combobox'
interface CollectionActionsProps {
minterContractAddress: string
sg721ContractAddress: string
sg721Messages: SG721Instance | undefined
vendingMinterMessages: VendingMinterInstance | undefined
baseMinterMessages: BaseMinterInstance | undefined
minterType: MinterType
}
type ExplicitContentType = true | false | undefined
@ -39,6 +43,8 @@ export const CollectionActions = ({
sg721Messages,
minterContractAddress,
vendingMinterMessages,
baseMinterMessages,
minterType,
}: CollectionActionsProps) => {
const wallet = useWallet()
const [lastTx, setLastTx] = useState('')
@ -88,6 +94,14 @@ export const CollectionActions = ({
subtitle: 'Address of the recipient',
})
const tokenURIState = useInputState({
id: 'token-uri',
name: 'tokenURI',
title: 'Token URI',
subtitle: 'URI for the token to be minted',
placeholder: 'ipfs://',
})
const whitelistState = useInputState({
id: 'whitelist-address',
name: 'whitelistAddress',
@ -138,6 +152,7 @@ export const CollectionActions = ({
placeholder: '8%',
})
const showTokenUriField = type === 'mint_token_uri'
const showWhitelistField = type === 'set_whitelist'
const showDateField = isEitherType(type, ['update_start_time', 'update_start_trading_time'])
const showLimitField = type === 'update_per_address_limit'
@ -168,8 +183,10 @@ export const CollectionActions = ({
sg721Contract: sg721ContractAddress,
tokenId: tokenIdState.value,
tokenIds: tokenIdListState.value,
tokenUri: tokenURIState.value,
batchNumber: batchNumberState.value,
vendingMinterMessages,
baseMinterMessages,
sg721Messages,
recipient: recipientState.value,
recipients: airdropArray,
@ -261,8 +278,9 @@ export const CollectionActions = ({
<form>
<div className="grid grid-cols-2 mt-4">
<div className="mr-2">
<ActionsCombobox {...actionComboboxState} />
<ActionsCombobox minterType={minterType} {...actionComboboxState} />
{showRecipientField && <AddressInput {...recipientState} />}
{showTokenUriField && <TextInput className="mt-2" {...tokenURIState} />}
{showWhitelistField && <AddressInput {...whitelistState} />}
{showLimitField && <NumberInput {...limitState} />}
{showTokenIdField && <NumberInput {...tokenIdState} />}
@ -334,7 +352,7 @@ export const CollectionActions = ({
</FormGroup>
)}
<Conditional test={showDateField}>
<FormControl htmlId="start-date" subtitle="Start time for the minting" title="Start Time">
<FormControl className="mt-2" htmlId="start-date" title="Start Time">
<InputDateTime minDate={new Date()} onChange={(date) => setTimestamp(date)} value={timestamp} />
</FormControl>
</Conditional>

View File

@ -2,19 +2,31 @@ import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { FormControl } from 'components/FormControl'
import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react'
import { Fragment, useEffect, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
import type { ActionListItem } from './actions'
import { ACTION_LIST } from './actions'
import { BASE_ACTION_LIST, VENDING_ACTION_LIST } from './actions'
export type MinterType = 'base' | 'vending'
export interface ActionsComboboxProps {
value: ActionListItem | null
onChange: (item: ActionListItem) => void
minterType?: MinterType
}
export const ActionsCombobox = ({ value, onChange }: ActionsComboboxProps) => {
export const ActionsCombobox = ({ value, onChange, minterType }: ActionsComboboxProps) => {
const [search, setSearch] = useState('')
const [ACTION_LIST, SET_ACTION_LIST] = useState<ActionListItem[]>(VENDING_ACTION_LIST)
useEffect(() => {
if (minterType === 'base') {
SET_ACTION_LIST(BASE_ACTION_LIST)
} else {
SET_ACTION_LIST(VENDING_ACTION_LIST)
}
}, [minterType])
const filtered =
search === '' ? ACTION_LIST : matchSorter(ACTION_LIST, search, { keys: ['id', 'name', 'description'] })

View File

@ -1,12 +1,16 @@
import { useBaseMinterContract } from 'contracts/baseMinter'
import type { CollectionInfo, SG721Instance } from 'contracts/sg721'
import { useSG721Contract } from 'contracts/sg721'
import type { VendingMinterInstance } from 'contracts/vendingMinter'
import { useVendingMinterContract } from 'contracts/vendingMinter'
import type { BaseMinterInstance } from '../../../contracts/baseMinter/contract'
export type ActionType = typeof ACTION_TYPES[number]
export const ACTION_TYPES = [
'mint',
'mint_token_uri',
'purge',
'update_mint_price',
'mint_to',
@ -35,7 +39,55 @@ export interface ActionListItem {
description?: string
}
export const ACTION_LIST: ActionListItem[] = [
export const BASE_ACTION_LIST: ActionListItem[] = [
{
id: 'mint_token_uri',
name: 'Mint Token URI',
description: `Mint a token with the given token URI`,
},
{
id: 'update_start_trading_time',
name: 'Update Trading Start Time',
description: `Update start time for trading`,
},
{
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: 'withdraw',
name: 'Withdraw Tokens',
description: `Withdraw tokens from the contract`,
},
{
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`,
},
]
export const VENDING_ACTION_LIST: ActionListItem[] = [
{
id: 'mint',
name: 'Mint',
@ -150,16 +202,18 @@ export interface DispatchExecuteProps {
type Select<T extends ActionType> = T
/** @see {@link VendingMinterInstance} */
/** @see {@link VendingMinterInstance}{@link BaseMinterInstance} */
export type DispatchExecuteArgs = {
minterContract: string
sg721Contract: string
vendingMinterMessages?: VendingMinterInstance
baseMinterMessages?: BaseMinterInstance
sg721Messages?: SG721Instance
txSigner: string
} & (
| { type: undefined }
| { type: Select<'mint'> }
| { type: Select<'mint_token_uri'>; tokenUri: string }
| { type: Select<'purge'> }
| { type: Select<'update_mint_price'>; price: string }
| { type: Select<'mint_to'>; recipient: string }
@ -183,14 +237,17 @@ export type DispatchExecuteArgs = {
)
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
const { vendingMinterMessages, sg721Messages, txSigner } = args
if (!vendingMinterMessages || !sg721Messages) {
const { vendingMinterMessages, baseMinterMessages, sg721Messages, txSigner } = args
if (!vendingMinterMessages || !baseMinterMessages || !sg721Messages) {
throw new Error('Cannot execute actions')
}
switch (args.type) {
case 'mint': {
return vendingMinterMessages.mint(txSigner)
}
case 'mint_token_uri': {
return baseMinterMessages.mint(txSigner, args.tokenUri)
}
case 'purge': {
return vendingMinterMessages.purge(txSigner)
}
@ -262,11 +319,16 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
const { messages: vendingMinterMessages } = useVendingMinterContract()
// eslint-disable-next-line react-hooks/rules-of-hooks
const { messages: sg721Messages } = useSG721Contract()
// eslint-disable-next-line react-hooks/rules-of-hooks
const { messages: baseMinterMessages } = useBaseMinterContract()
const { minterContract, sg721Contract } = args
switch (args.type) {
case 'mint': {
return vendingMinterMessages(minterContract)?.mint()
}
case 'mint_token_uri': {
return baseMinterMessages(minterContract)?.mint(args.tokenUri)
}
case 'purge': {
return vendingMinterMessages(minterContract)?.purge()
}

View File

@ -2,19 +2,30 @@ import { Combobox, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { FormControl } from 'components/FormControl'
import { matchSorter } from 'match-sorter'
import { Fragment, useState } from 'react'
import { Fragment, useEffect, useState } from 'react'
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
import type { MinterType } from '../actions/Combobox'
import type { QueryListItem } from './query'
import { QUERY_LIST } from './query'
import { BASE_QUERY_LIST, VENDING_QUERY_LIST } from './query'
export interface QueryComboboxProps {
value: QueryListItem | null
onChange: (item: QueryListItem) => void
minterType?: MinterType
}
export const QueryCombobox = ({ value, onChange }: QueryComboboxProps) => {
export const QueryCombobox = ({ value, onChange, minterType }: QueryComboboxProps) => {
const [search, setSearch] = useState('')
const [QUERY_LIST, SET_QUERY_LIST] = useState<QueryListItem[]>(VENDING_QUERY_LIST)
useEffect(() => {
if (minterType === 'base') {
SET_QUERY_LIST(BASE_QUERY_LIST)
} else {
SET_QUERY_LIST(VENDING_QUERY_LIST)
}
}, [minterType])
const filtered = search === '' ? QUERY_LIST : matchSorter(QUERY_LIST, search, { keys: ['id', 'name', 'description'] })

View File

@ -5,22 +5,29 @@ import { FormControl } from 'components/FormControl'
import { AddressInput, TextInput } from 'components/forms/FormInput'
import { useInputState } from 'components/forms/FormInput.hooks'
import { JsonPreview } from 'components/JsonPreview'
import type { BaseMinterInstance } from 'contracts/baseMinter'
import type { SG721Instance } from 'contracts/sg721'
import type { MinterInstance } from 'contracts/vendingMinter'
import type { VendingMinterInstance } from 'contracts/vendingMinter'
import { toast } from 'react-hot-toast'
import { useQuery } from 'react-query'
import type { MinterType } from '../actions/Combobox'
interface CollectionQueriesProps {
minterContractAddress: string
sg721ContractAddress: string
sg721Messages: SG721Instance | undefined
minterMessages: MinterInstance | undefined
vendingMinterMessages: VendingMinterInstance | undefined
baseMinterMessages: BaseMinterInstance | undefined
minterType: MinterType
}
export const CollectionQueries = ({
sg721ContractAddress,
sg721Messages,
minterContractAddress,
minterMessages,
vendingMinterMessages,
baseMinterMessages,
minterType,
}: CollectionQueriesProps) => {
const comboboxState = useQueryComboboxState()
const type = comboboxState.value?.id
@ -46,12 +53,14 @@ export const CollectionQueries = ({
const showAddressField = type === 'tokens_minted_to_user'
const { data: response } = useQuery(
[sg721Messages, minterMessages, type, tokenId, address] as const,
[sg721Messages, baseMinterMessages, vendingMinterMessages, type, tokenId, address] as const,
async ({ queryKey }) => {
const [_sg721Messages, _minterMessages, _type, _tokenId, _address] = queryKey
const [_sg721Messages, _baseMinterMessages_, _vendingMinterMessages, _type, _tokenId, _address] = queryKey
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const result = await dispatchQuery({
tokenId: _tokenId,
minterMessages: _minterMessages,
vendingMinterMessages: _vendingMinterMessages,
baseMinterMessages: _baseMinterMessages_,
sg721Messages: _sg721Messages,
address: _address,
type: _type,
@ -71,7 +80,7 @@ export const CollectionQueries = ({
return (
<div className="grid grid-cols-2 mt-4">
<div className="mr-2 space-y-8">
<QueryCombobox {...comboboxState} />
<QueryCombobox minterType={minterType} {...comboboxState} />
{showAddressField && <AddressInput {...addressState} />}
{showTokenIdField && <TextInput {...tokenIdState} />}
</div>

View File

@ -1,3 +1,4 @@
import type { BaseMinterInstance } from 'contracts/baseMinter'
import type { SG721Instance } from 'contracts/sg721'
import type { VendingMinterInstance } from 'contracts/vendingMinter'
@ -10,6 +11,8 @@ export const QUERY_TYPES = [
'tokens_minted_to_user',
// 'token_owners',
'token_info',
'config',
'status',
] as const
export interface QueryListItem {
@ -18,7 +21,7 @@ export interface QueryListItem {
description?: string
}
export const QUERY_LIST: QueryListItem[] = [
export const VENDING_QUERY_LIST: QueryListItem[] = [
{
id: 'collection_info',
name: 'Collection Info',
@ -49,6 +52,43 @@ export const QUERY_LIST: QueryListItem[] = [
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 const BASE_QUERY_LIST: QueryListItem[] = [
{
id: 'collection_info',
name: 'Collection Info',
description: `Get information about the collection.`,
},
{
id: 'tokens_minted_to_user',
name: 'Tokens Minted to User',
description: `Get the number of tokens minted in the collection to a user.`,
},
{
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 {
@ -59,6 +99,7 @@ export interface DispatchExecuteProps {
type Select<T extends QueryType> = T
export type DispatchQueryArgs = {
baseMinterMessages?: BaseMinterInstance
vendingMinterMessages?: VendingMinterInstance
sg721Messages?: SG721Instance
} & (
@ -69,11 +110,13 @@ export type DispatchQueryArgs = {
| { type: Select<'tokens_minted_to_user'>; address: string }
// | { type: Select<'token_owners'> }
| { type: Select<'token_info'>; tokenId: string }
| { type: Select<'config'> }
| { type: Select<'status'> }
)
export const dispatchQuery = async (args: DispatchQueryArgs) => {
const { vendingMinterMessages, sg721Messages } = args
if (!vendingMinterMessages || !sg721Messages) {
const { baseMinterMessages, vendingMinterMessages, sg721Messages } = args
if (!baseMinterMessages || !vendingMinterMessages || !sg721Messages) {
throw new Error('Cannot execute actions')
}
switch (args.type) {
@ -96,6 +139,12 @@ export const dispatchQuery = async (args: DispatchQueryArgs) => {
if (!args.tokenId) return
return sg721Messages.allNftInfo(args.tokenId)
}
case 'config': {
return baseMinterMessages.getConfig()
}
case 'status': {
return baseMinterMessages.getStatus()
}
default: {
throw new Error('Unknown action')
}

View File

@ -1,3 +1,4 @@
import { toUtf8 } from '@cosmjs/encoding'
import { CollectionActions } from 'components/collections/actions/Action'
import { CollectionQueries } from 'components/collections/queries/Queries'
import { ContractPageHeader } from 'components/ContractPageHeader'
@ -9,14 +10,19 @@ import type { NextPage } from 'next'
import { useRouter } from 'next/router'
import { NextSeo } from 'next-seo'
import { useEffect, useMemo, useState } from 'react'
import toast from 'react-hot-toast'
import { useDebounce } from 'utils/debounce'
import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
import type { MinterType } from '../../components/collections/actions/Combobox'
const CollectionActionsPage: NextPage = () => {
const { vendingMinter: vendingMinterContract, sg721: sg721Contract } = useContracts()
const { baseMinter: baseMinterContract, vendingMinter: vendingMinterContract, sg721: sg721Contract } = useContracts()
const wallet = useWallet()
const [action, setAction] = useState<boolean>(false)
const [minterType, setMinterType] = useState<MinterType>('vending')
const sg721ContractState = useInputState({
id: 'sg721-contract-address',
@ -32,10 +38,16 @@ const CollectionActionsPage: NextPage = () => {
subtitle: 'Address of the Minter contract',
})
const debouncedMinterContractState = useDebounce(minterContractState.value, 300)
const vendingMinterMessages = useMemo(
() => vendingMinterContract?.use(minterContractState.value),
[vendingMinterContract, minterContractState.value],
)
const baseMinterMessages = useMemo(
() => baseMinterContract?.use(minterContractState.value),
[baseMinterContract, minterContractState.value],
)
const sg721Messages = useMemo(
() => sg721Contract?.use(sg721ContractState.value),
[sg721Contract, sg721ContractState.value],
@ -66,6 +78,41 @@ const CollectionActionsPage: NextPage = () => {
if (initialSg721 && initialSg721.length > 0) sg721ContractState.onChange(initialSg721)
}, [])
useEffect(() => {
async function getMinterContractType() {
if (wallet.client && debouncedMinterContractState.length > 0) {
const client = wallet.client
const data = await toast.promise(
client.queryContractRaw(
debouncedMinterContractState,
toUtf8(Buffer.from(Buffer.from('contract_info').toString('hex'), 'hex').toString()),
),
{
loading: 'Retrieving Minter type...',
error: 'Minter type retrieval failed.',
success: 'Minter type retrieved.',
},
)
const contract: string = JSON.parse(new TextDecoder().decode(data as Uint8Array)).contract
console.log(contract)
return contract
}
}
void getMinterContractType()
.then((contract) => {
if (contract?.includes('sg-base-minter')) {
setMinterType('base')
} else {
setMinterType('vending')
}
})
.catch((err) => {
console.log(err)
setMinterType('vending')
console.log('Unable to retrieve contract version')
})
}, [debouncedMinterContractState, wallet.address])
return (
<section className="py-6 px-12 space-y-4">
<NextSeo title="Collection Actions" />
@ -124,18 +171,23 @@ const CollectionActionsPage: NextPage = () => {
</div>
<div>
{(action && (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
<CollectionActions
baseMinterMessages={baseMinterMessages}
minterContractAddress={minterContractState.value}
minterMessages={vendingMinterMessages}
minterType={minterType}
sg721ContractAddress={sg721ContractState.value}
sg721Messages={sg721Messages}
vendingMinterMessages={vendingMinterMessages}
/>
)) || (
<CollectionQueries
baseMinterMessages={baseMinterMessages}
minterContractAddress={minterContractState.value}
minterMessages={vendingMinterMessages}
minterType={minterType}
sg721ContractAddress={sg721ContractState.value}
sg721Messages={sg721Messages}
vendingMinterMessages={vendingMinterMessages}
/>
)}
</div>

View File

@ -456,7 +456,7 @@ const CollectionCreationPage: NextPage = () => {
<Anchor
className="text-stargaze hover:underline"
external
href={`/contracts/minter/query/?contractAddress=${vendingMinterContractAddress as string}`}
href={`/contracts/vendingMinter/query/?contractAddress=${vendingMinterContractAddress as string}`}
>
{vendingMinterContractAddress}
</Anchor>

View File

@ -16,7 +16,7 @@ import { withMetadata } from 'utils/layout'
import { links } from 'utils/links'
const CollectionQueriesPage: NextPage = () => {
const { vendingMinter: vendingMinterContract, sg721: sg721Contract } = useContracts()
const { baseMinter: baseMinterContract, vendingMinter: vendingMinterContract, sg721: sg721Contract } = useContracts()
const comboboxState = useQueryComboboxState()
const type = comboboxState.value?.id
@ -61,15 +61,21 @@ const CollectionQueriesPage: NextPage = () => {
() => vendingMinterContract?.use(minterContractAddress),
[vendingMinterContract, minterContractAddress],
)
const baseMinterMessages = useMemo(
() => baseMinterContract?.use(minterContractAddress),
[baseMinterContract, minterContractAddress],
)
const sg721Messages = useMemo(() => sg721Contract?.use(sg721ContractAddress), [sg721Contract, sg721ContractAddress])
const { data: response } = useQuery(
[sg721Messages, vendingMinterMessages, type, tokenId, address] as const,
[sg721Messages, baseMinterMessages, vendingMinterMessages, type, tokenId, address] as const,
async ({ queryKey }) => {
const [_sg721Messages, _vendingMinterMessages, _type, _tokenId, _address] = queryKey
const [_sg721Messages, _baseMinterMessages_, _vendingMinterMessages, _type, _tokenId, _address] = queryKey
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const result = await dispatchQuery({
tokenId: _tokenId,
minterMessages: _vendingMinterMessages,
vendingMinterMessages: _vendingMinterMessages,
baseMinterMessages: _baseMinterMessages_,
sg721Messages: _sg721Messages,
address: _address,
type: _type,