From c9d473441777537fc898f9581fe09f9ff45be2f8 Mon Sep 17 00:00:00 2001 From: Serkan Reis Date: Fri, 9 Dec 2022 21:47:03 +0300 Subject: [PATCH] Implement Collection Actions changes for Base Minter contract --- components/collections/actions/Action.tsx | 22 ++++++- components/collections/actions/Combobox.tsx | 18 +++++- components/collections/actions/actions.ts | 70 +++++++++++++++++++-- components/collections/queries/Combobox.tsx | 17 ++++- components/collections/queries/Queries.tsx | 23 ++++--- components/collections/queries/query.ts | 55 +++++++++++++++- pages/collections/actions.tsx | 58 ++++++++++++++++- pages/collections/create.tsx | 2 +- pages/collections/queries.tsx | 14 +++-- 9 files changed, 249 insertions(+), 30 deletions(-) diff --git a/components/collections/actions/Action.tsx b/components/collections/actions/Action.tsx index 69d6b62..513d065 100644 --- a/components/collections/actions/Action.tsx +++ b/components/collections/actions/Action.tsx @@ -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 = ({
- + {showRecipientField && } + {showTokenUriField && } {showWhitelistField && } {showLimitField && } {showTokenIdField && } @@ -334,7 +352,7 @@ export const CollectionActions = ({ )} - + setTimestamp(date)} value={timestamp} /> diff --git a/components/collections/actions/Combobox.tsx b/components/collections/actions/Combobox.tsx index 2e347c6..4536808 100644 --- a/components/collections/actions/Combobox.tsx +++ b/components/collections/actions/Combobox.tsx @@ -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(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'] }) diff --git a/components/collections/actions/actions.ts b/components/collections/actions/actions.ts index b664724..35d45df 100644 --- a/components/collections/actions/actions.ts +++ b/components/collections/actions/actions.ts @@ -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 -/** @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() } diff --git a/components/collections/queries/Combobox.tsx b/components/collections/queries/Combobox.tsx index 62485eb..d5ce10c 100644 --- a/components/collections/queries/Combobox.tsx +++ b/components/collections/queries/Combobox.tsx @@ -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(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'] }) diff --git a/components/collections/queries/Queries.tsx b/components/collections/queries/Queries.tsx index b9ca571..931e661 100644 --- a/components/collections/queries/Queries.tsx +++ b/components/collections/queries/Queries.tsx @@ -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 (
- + {showAddressField && } {showTokenIdField && }
diff --git a/components/collections/queries/query.ts b/components/collections/queries/query.ts index ad1482b..37b041b 100644 --- a/components/collections/queries/query.ts +++ b/components/collections/queries/query.ts @@ -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 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') } diff --git a/pages/collections/actions.tsx b/pages/collections/actions.tsx index d6ce4b5..0250853 100644 --- a/pages/collections/actions.tsx +++ b/pages/collections/actions.tsx @@ -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(false) + const [minterType, setMinterType] = useState('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 (
@@ -124,18 +171,23 @@ const CollectionActionsPage: NextPage = () => {
{(action && ( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition )) || ( )}
diff --git a/pages/collections/create.tsx b/pages/collections/create.tsx index 730e99f..aa9528d 100644 --- a/pages/collections/create.tsx +++ b/pages/collections/create.tsx @@ -456,7 +456,7 @@ const CollectionCreationPage: NextPage = () => { {vendingMinterContractAddress} diff --git a/pages/collections/queries.tsx b/pages/collections/queries.tsx index 40a2ebb..ef2697e 100644 --- a/pages/collections/queries.tsx +++ b/pages/collections/queries.tsx @@ -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,