diff --git a/components/collections/actions/Action.tsx b/components/collections/actions/Action.tsx new file mode 100644 index 0000000..80aa634 --- /dev/null +++ b/components/collections/actions/Action.tsx @@ -0,0 +1,188 @@ +import { Button } from 'components/Button' +import type { DispatchExecuteArgs } from 'components/collections/actions/actions' +import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/collections/actions/actions' +import { ActionsCombobox } from 'components/collections/actions/Combobox' +import { useActionsComboboxState } from 'components/collections/actions/Combobox.hooks' +import { Conditional } from 'components/Conditional' +import { FormControl } from 'components/FormControl' +import { FormGroup } from 'components/FormGroup' +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 { TransactionHash } from 'components/TransactionHash' +import { WhitelistUpload } from 'components/WhitelistUpload' +import { useWallet } from 'contexts/wallet' +import type { MinterInstance } from 'contracts/minter' +import type { SG721Instance } from 'contracts/sg721' +import type { FormEvent } from 'react' +import { useState } from 'react' +import { toast } from 'react-hot-toast' +import { FaArrowRight } from 'react-icons/fa' +import { useMutation } from 'react-query' + +import { TextInput } from '../../forms/FormInput' + +interface CollectionActionsProps { + minterContractAddress: string + sg721ContractAddress: string + sg721Messages: SG721Instance | undefined + minterMessages: MinterInstance | undefined +} + +export const CollectionActions = ({ + sg721ContractAddress, + sg721Messages, + minterContractAddress, + minterMessages, +}: CollectionActionsProps) => { + const wallet = useWallet() + const [lastTx, setLastTx] = useState('') + + const [timestamp, setTimestamp] = useState(undefined) + const [airdropArray, setAirdropArray] = useState([]) + + const actionComboboxState = useActionsComboboxState() + const type = actionComboboxState.value?.id + + const limitState = useNumberInputState({ + id: 'per-address-limi', + 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 batchNumberState = useNumberInputState({ + id: 'batch-number', + name: 'batchNumber', + title: 'Number of Tokens', + subtitle: 'Enter the number of tokens to mint', + }) + + const tokenIdListState = useInputState({ + id: 'token-id-list', + name: 'tokenIdList', + title: 'List of token IDs', + subtitle: + 'Specify individual token IDs separated by commas (e.g., 2, 4, 8) or a range of IDs separated by a colon (e.g., 8:13)', + }) + + const recipientState = useInputState({ + id: 'recipient-address', + name: 'recipient', + title: 'Recipient Address', + subtitle: 'Address of the recipient', + }) + + const whitelistState = useInputState({ + id: 'whitelist-address', + name: 'whitelistAddress', + title: 'Whitelist Address', + subtitle: 'Address of the whitelist contract', + }) + + const showWhitelistField = type === 'set_whitelist' + const showDateField = type === 'update_start_time' + const showLimitField = type === 'update_per_address_limit' + const showTokenIdField = isEitherType(type, ['transfer', 'mint_for', 'burn']) + const showNumberOfTokensField = type === 'batch_mint' + const showTokenIdListField = isEitherType(type, ['batch_burn', 'batch_transfer']) + const showRecipientField = isEitherType(type, ['transfer', 'mint_to', 'mint_for', 'batch_mint', 'batch_transfer']) + const showAirdropFileField = type === 'airdrop' + + const payload: DispatchExecuteArgs = { + whitelist: whitelistState.value, + startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '', + limit: limitState.value, + minterContract: minterContractAddress, + sg721Contract: sg721ContractAddress, + tokenId: tokenIdState.value, + tokenIds: tokenIdListState.value, + batchNumber: batchNumberState.value, + minterMessages, + sg721Messages, + recipient: recipientState.value, + recipients: airdropArray, + txSigner: wallet.address, + type, + } + const { isLoading, mutate } = useMutation( + async (event: FormEvent) => { + event.preventDefault() + if (!type) { + throw new Error('Please select an action!') + } + if (minterContractAddress === '' && sg721ContractAddress === '') { + throw new Error('Please enter minter and sg721 contract addresses!') + } + 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)) + }, + }, + ) + + const airdropFileOnChange = (data: string[]) => { + setAirdropArray(data) + } + + return ( +
+
+
+ + {showRecipientField && } + {showWhitelistField && } + {showLimitField && } + {showTokenIdField && } + {showTokenIdListField && } + {showNumberOfTokensField && } + {showAirdropFileField && ( + + + + )} + + + setTimestamp(date)} value={timestamp} /> + + +
+
+
+ + + + +
+ + + +
+
+
+ ) +} diff --git a/components/collections/actions/Combobox.tsx b/components/collections/actions/Combobox.tsx index 2f59969..2e347c6 100644 --- a/components/collections/actions/Combobox.tsx +++ b/components/collections/actions/Combobox.tsx @@ -26,7 +26,7 @@ export const ActionsCombobox = ({ value, onChange }: ActionsComboboxProps) => { labelAs={Combobox.Label} onChange={onChange} subtitle="Collection actions" - title="Action" + title="" value={value} >
diff --git a/components/collections/queries/Combobox.tsx b/components/collections/queries/Combobox.tsx index ca65511..62485eb 100644 --- a/components/collections/queries/Combobox.tsx +++ b/components/collections/queries/Combobox.tsx @@ -25,7 +25,7 @@ export const QueryCombobox = ({ value, onChange }: QueryComboboxProps) => { labelAs={Combobox.Label} onChange={onChange} subtitle="Collection queries" - title="Query" + title="" value={value} >
diff --git a/components/collections/queries/Queries.tsx b/components/collections/queries/Queries.tsx new file mode 100644 index 0000000..d5f6e5b --- /dev/null +++ b/components/collections/queries/Queries.tsx @@ -0,0 +1,85 @@ +import { QueryCombobox } from 'components/collections/queries/Combobox' +import { useQueryComboboxState } from 'components/collections/queries/Combobox.hooks' +import { dispatchQuery } from 'components/collections/queries/query' +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 { MinterInstance } from 'contracts/minter' +import type { SG721Instance } from 'contracts/sg721' +import { toast } from 'react-hot-toast' +import { useQuery } from 'react-query' + +interface CollectionQueriesProps { + minterContractAddress: string + sg721ContractAddress: string + sg721Messages: SG721Instance | undefined + minterMessages: MinterInstance | undefined +} +export const CollectionQueries = ({ + sg721ContractAddress, + sg721Messages, + minterContractAddress, + minterMessages, +}: CollectionQueriesProps) => { + const comboboxState = useQueryComboboxState() + const type = comboboxState.value?.id + + const tokenIdState = useInputState({ + id: 'token-id', + name: 'tokenId', + title: 'Token ID', + subtitle: 'Enter the token ID', + placeholder: '1', + }) + const tokenId = tokenIdState.value + + const addressState = useInputState({ + id: 'address', + name: 'address', + title: 'User Address', + subtitle: 'Address of the user', + }) + const address = addressState.value + + const showTokenIdField = type === 'token_info' + const showAddressField = type === 'tokens_minted_to_user' + + const { data: response } = useQuery( + [sg721Messages, minterMessages, type, tokenId, address] as const, + async ({ queryKey }) => { + const [_sg721Messages, _minterMessages, _type, _tokenId, _address] = queryKey + const result = await dispatchQuery({ + tokenId: _tokenId, + minterMessages: _minterMessages, + sg721Messages: _sg721Messages, + address: _address, + type: _type, + }) + return result + }, + { + placeholderData: null, + onError: (error: any) => { + toast.error(error.message) + }, + enabled: Boolean(sg721ContractAddress && minterContractAddress && type), + retry: false, + }, + ) + + return ( +
+
+ + {showAddressField && } + {showTokenIdField && } +
+
+ + + +
+
+ ) +} diff --git a/env. b/env. new file mode 100644 index 0000000..95986a6 --- /dev/null +++ b/env. @@ -0,0 +1,18 @@ +APP_VERSION=0.1.0 + +NEXT_PUBLIC_PINATA_ENDPOINT_URL=https://api.pinata.cloud/pinning/pinFileToIPFS +NEXT_PUBLIC_WHITELIST_CODE_ID=3 +NEXT_PUBLIC_MINTER_CODE_ID=2 +NEXT_PUBLIC_SG721_CODE_ID=1 + +NEXT_PUBLIC_API_URL=https:// +NEXT_PUBLIC_BLOCK_EXPLORER_URL=https://testnet-explorer.publicawesome.dev/stargaze +NEXT_PUBLIC_NETWORK=testnet +NEXT_PUBLIC_STARGAZE_WEBSITE_URL=https://testnet.publicawesome.dev +NEXT_PUBLIC_WEBSITE_URL=https:// + +NEXT_PUBLIC_S3_BUCKET= # TODO +NEXT_PUBLIC_S3_ENDPOINT= # TODO +NEXT_PUBLIC_S3_KEY= # TODO +NEXT_PUBLIC_S3_REGION= # TODO +NEXT_PUBLIC_S3_SECRET= # TODO \ No newline at end of file diff --git a/pages/collections/actions.tsx b/pages/collections/actions.tsx index e24bbdb..8757da5 100644 --- a/pages/collections/actions.tsx +++ b/pages/collections/actions.tsx @@ -1,42 +1,22 @@ -import { Button } from 'components/Button' -import type { DispatchExecuteArgs } from 'components/collections/actions/actions' -import { dispatchExecute, isEitherType, previewExecutePayload } from 'components/collections/actions/actions' -import { ActionsCombobox } from 'components/collections/actions/Combobox' -import { useActionsComboboxState } from 'components/collections/actions/Combobox.hooks' -import { Conditional } from 'components/Conditional' +import { CollectionActions } from 'components/collections/actions/Action' +import { CollectionQueries } from 'components/collections/queries/Queries' import { ContractPageHeader } from 'components/ContractPageHeader' -import { FormControl } from 'components/FormControl' -import { FormGroup } from 'components/FormGroup' -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 { TransactionHash } from 'components/TransactionHash' -import { WhitelistUpload } from 'components/WhitelistUpload' +import { AddressInput } from 'components/forms/FormInput' +import { useInputState } from 'components/forms/FormInput.hooks' import { useContracts } from 'contexts/contracts' import { useWallet } from 'contexts/wallet' import type { NextPage } from 'next' +import { useRouter } from 'next/router' import { NextSeo } from 'next-seo' -import type { FormEvent } from 'react' -import { useMemo, useState } from 'react' -import { toast } from 'react-hot-toast' -import { FaArrowRight } from 'react-icons/fa' -import { useMutation } from 'react-query' +import { useEffect, useMemo, useState } from 'react' import { withMetadata } from 'utils/layout' import { links } from 'utils/links' -import { TextInput } from '../../components/forms/FormInput' - const CollectionActionsPage: NextPage = () => { const { minter: minterContract, sg721: sg721Contract } = useContracts() const wallet = useWallet() - const [lastTx, setLastTx] = useState('') - const [timestamp, setTimestamp] = useState(undefined) - const [airdropArray, setAirdropArray] = useState([]) - - const comboboxState = useActionsComboboxState() - const type = comboboxState.value?.id + const [action, setAction] = useState(false) const sg721ContractState = useInputState({ id: 'sg721-contract-address', @@ -52,58 +32,6 @@ const CollectionActionsPage: NextPage = () => { subtitle: 'Address of the Minter contract', }) - const limitState = useNumberInputState({ - id: 'per-address-limi', - 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 batchNumberState = useNumberInputState({ - id: 'batch-number', - name: 'batchNumber', - title: 'Number of Tokens', - subtitle: 'Enter the number of tokens to mint', - }) - - const tokenIdListState = useInputState({ - id: 'token-id-list', - name: 'tokenIdList', - title: 'List of token IDs', - subtitle: - 'Specify individual token IDs separated by commas (e.g., 2, 4, 8) or a range of IDs separated by a colon (e.g., 8:13)', - }) - - const recipientState = useInputState({ - id: 'recipient-address', - name: 'recipient', - title: 'Recipient Address', - subtitle: 'Address of the recipient', - }) - - const whitelistState = useInputState({ - id: 'whitelist-address', - name: 'whitelistAddress', - title: 'Whitelist Address', - subtitle: 'Address of the whitelist contract', - }) - - const showWhitelistField = type === 'set_whitelist' - const showDateField = type === 'update_start_time' - const showLimitField = type === 'update_per_address_limit' - const showTokenIdField = isEitherType(type, ['transfer', 'mint_for', 'burn']) - const showNumberOfTokensField = type === 'batch_mint' - const showTokenIdListField = isEitherType(type, ['batch_burn', 'batch_transfer']) - const showRecipientField = isEitherType(type, ['transfer', 'mint_to', 'mint_for', 'batch_mint', 'batch_transfer']) - const showAirdropFileField = type === 'airdrop' - const minterMessages = useMemo( () => minterContract?.use(minterContractState.value), [minterContract, minterContractState.value], @@ -112,50 +40,31 @@ const CollectionActionsPage: NextPage = () => { () => sg721Contract?.use(sg721ContractState.value), [sg721Contract, sg721ContractState.value], ) - const payload: DispatchExecuteArgs = { - whitelist: whitelistState.value, - startTime: timestamp ? (timestamp.getTime() * 1_000_000).toString() : '', - limit: limitState.value, - minterContract: minterContractState.value, - sg721Contract: sg721ContractState.value, - tokenId: tokenIdState.value, - tokenIds: tokenIdListState.value, - batchNumber: batchNumberState.value, - minterMessages, - sg721Messages, - recipient: recipientState.value, - recipients: airdropArray, - txSigner: wallet.address, - type, - } - const { isLoading, mutate } = useMutation( - async (event: FormEvent) => { - event.preventDefault() - if (!type) { - throw new Error('Please select an action!') - } - if (minterContractState.value === '' && sg721ContractState.value === '') { - throw new Error('Please enter minter and sg721 contract addresses!') - } - 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)) - }, - }, - ) - const airdropFileOnChange = (data: string[]) => { - setAirdropArray(data) - } + const sg721ContractAddress = sg721ContractState.value + const minterContractAddress = minterContractState.value + + const router = useRouter() + + useEffect(() => { + if (minterContractAddress.length > 0 && sg721ContractAddress.length === 0) { + void router.replace({ query: { minterContractAddress } }) + } + if (sg721ContractAddress.length > 0 && minterContractAddress.length === 0) { + void router.replace({ query: { sg721ContractAddress } }) + } + if (sg721ContractAddress.length > 0 && minterContractAddress.length > 0) { + void router.replace({ query: { sg721ContractAddress, minterContractAddress } }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sg721ContractAddress, minterContractAddress]) + + useEffect(() => { + const initialMinter = new URL(document.URL).searchParams.get('minterContractAddress') + const initialSg721 = new URL(document.URL).searchParams.get('sg721ContractAddress') + if (initialMinter && initialMinter.length > 0) minterContractState.onChange(initialMinter) + if (initialSg721 && initialSg721.length > 0) sg721ContractState.onChange(initialSg721) + }, []) return (
@@ -166,40 +75,71 @@ const CollectionActionsPage: NextPage = () => { title="Collection Actions" /> -
-
- + +
+ - - {showRecipientField && } - {showWhitelistField && } - {showLimitField && } - {showTokenIdField && } - {showTokenIdListField && } - {showNumberOfTokensField && } - {showAirdropFileField && ( - - - - )} - - - setTimestamp(date)} value={timestamp} /> - -
-
-
- - - - +
+
+
+
+ { + setAction(false) + }} + type="radio" + value="false" + /> + +
+
+ { + setAction(true) + }} + type="radio" + value="true" + /> + +
+
+
+ {(action && ( + + )) || ( + + )} +
- - -