Implement Base Minter Contract dashboard
This commit is contained in:
@@ -18,26 +18,49 @@ export const sg721LinkTabs: LinkTabProps[] = [
|
||||
},
|
||||
]
|
||||
|
||||
export const minterLinkTabs: LinkTabProps[] = [
|
||||
export const vendingMinterLinkTabs: LinkTabProps[] = [
|
||||
{
|
||||
title: 'Instantiate',
|
||||
description: `Initialize a new Minter contract`,
|
||||
href: '/contracts/minter/instantiate',
|
||||
description: `Initialize a new Vending Minter contract`,
|
||||
href: '/contracts/vendingMinter/instantiate',
|
||||
},
|
||||
{
|
||||
title: 'Query',
|
||||
description: `Dispatch queries with your Minter contract`,
|
||||
href: '/contracts/minter/query',
|
||||
description: `Dispatch queries with your Vending Minter contract`,
|
||||
href: '/contracts/vendingMinter/query',
|
||||
},
|
||||
{
|
||||
title: 'Execute',
|
||||
description: `Execute Minter contract actions`,
|
||||
href: '/contracts/minter/execute',
|
||||
description: `Execute Vending Minter contract actions`,
|
||||
href: '/contracts/vendingMinter/execute',
|
||||
},
|
||||
{
|
||||
title: 'Migrate',
|
||||
description: `Migrate Minter contract`,
|
||||
href: '/contracts/minter/migrate',
|
||||
description: `Migrate Vending Minter contract`,
|
||||
href: '/contracts/vendingMinter/migrate',
|
||||
},
|
||||
]
|
||||
|
||||
export const baseMinterLinkTabs: LinkTabProps[] = [
|
||||
{
|
||||
title: 'Instantiate',
|
||||
description: `Initialize a new Base Minter contract`,
|
||||
href: '/contracts/baseMinter/instantiate',
|
||||
},
|
||||
{
|
||||
title: 'Query',
|
||||
description: `Dispatch queries with your Base Minter contract`,
|
||||
href: '/contracts/baseMinter/query',
|
||||
},
|
||||
{
|
||||
title: 'Execute',
|
||||
description: `Execute Base Minter contract actions`,
|
||||
href: '/contracts/baseMinter/execute',
|
||||
},
|
||||
{
|
||||
title: 'Migrate',
|
||||
description: `Migrate Base Minter contract`,
|
||||
href: '/contracts/baseMinter/migrate',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ const routes = [
|
||||
{ text: 'My Collections', href: `/collections/myCollections/`, isChild: true },
|
||||
{ text: 'Collection Actions', href: `/collections/actions/`, isChild: true },
|
||||
{ text: 'Contract Dashboards', href: `/contracts/`, isChild: false },
|
||||
{ text: 'Minter Contract', href: `/contracts/minter/`, isChild: true },
|
||||
{ text: 'Base Minter Contract', href: `/contracts/baseMinter/`, isChild: true },
|
||||
{ text: 'Vending Minter Contract', href: `/contracts/vendingMinter/`, isChild: true },
|
||||
{ text: 'SG721 Contract', href: `/contracts/sg721/`, isChild: true },
|
||||
{ text: 'Whitelist Contract', href: `/contracts/whitelist/`, isChild: true },
|
||||
]
|
||||
@@ -37,9 +38,9 @@ export const Sidebar = () => {
|
||||
<Anchor
|
||||
key={href}
|
||||
className={clsx(
|
||||
'px-4 -mx-5 font-extrabold uppercase rounded-lg', // styling
|
||||
'px-2 -mx-5 font-extrabold uppercase rounded-lg', // styling
|
||||
'hover:bg-white/5 transition-colors', // hover styling
|
||||
{ 'py-0 ml-2 text-sm font-bold': isChild },
|
||||
{ 'py-0 -ml-2 text-sm font-bold': isChild },
|
||||
{
|
||||
'text-gray hover:text-white':
|
||||
!router.asPath.substring(0, router.asPath.lastIndexOf('/') + 1).includes(href) && isChild,
|
||||
|
||||
@@ -13,8 +13,8 @@ import { InputDateTime } from 'components/InputDateTime'
|
||||
import { JsonPreview } from 'components/JsonPreview'
|
||||
import { TransactionHash } from 'components/TransactionHash'
|
||||
import { useWallet } from 'contexts/wallet'
|
||||
import type { MinterInstance } from 'contracts/minter'
|
||||
import type { SG721Instance } from 'contracts/sg721'
|
||||
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
||||
import type { FormEvent } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
@@ -29,7 +29,7 @@ interface CollectionActionsProps {
|
||||
minterContractAddress: string
|
||||
sg721ContractAddress: string
|
||||
sg721Messages: SG721Instance | undefined
|
||||
minterMessages: MinterInstance | undefined
|
||||
vendingMinterMessages: VendingMinterInstance | undefined
|
||||
}
|
||||
|
||||
type ExplicitContentType = true | false | undefined
|
||||
@@ -38,7 +38,7 @@ export const CollectionActions = ({
|
||||
sg721ContractAddress,
|
||||
sg721Messages,
|
||||
minterContractAddress,
|
||||
minterMessages,
|
||||
vendingMinterMessages,
|
||||
}: CollectionActionsProps) => {
|
||||
const wallet = useWallet()
|
||||
const [lastTx, setLastTx] = useState('')
|
||||
@@ -169,7 +169,7 @@ export const CollectionActions = ({
|
||||
tokenId: tokenIdState.value,
|
||||
tokenIds: tokenIdListState.value,
|
||||
batchNumber: batchNumberState.value,
|
||||
minterMessages,
|
||||
vendingMinterMessages,
|
||||
sg721Messages,
|
||||
recipient: recipientState.value,
|
||||
recipients: airdropArray,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { MinterInstance } from 'contracts/minter'
|
||||
import { useMinterContract } from 'contracts/minter'
|
||||
import type { CollectionInfo, SG721Instance } from 'contracts/sg721'
|
||||
import { useSG721Contract } from 'contracts/sg721'
|
||||
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
||||
import { useVendingMinterContract } from 'contracts/vendingMinter'
|
||||
|
||||
export type ActionType = typeof ACTION_TYPES[number]
|
||||
|
||||
@@ -150,11 +150,11 @@ export interface DispatchExecuteProps {
|
||||
|
||||
type Select<T extends ActionType> = T
|
||||
|
||||
/** @see {@link MinterInstance} */
|
||||
/** @see {@link VendingMinterInstance} */
|
||||
export type DispatchExecuteArgs = {
|
||||
minterContract: string
|
||||
sg721Contract: string
|
||||
minterMessages?: MinterInstance
|
||||
vendingMinterMessages?: VendingMinterInstance
|
||||
sg721Messages?: SG721Instance
|
||||
txSigner: string
|
||||
} & (
|
||||
@@ -183,40 +183,40 @@ export type DispatchExecuteArgs = {
|
||||
)
|
||||
|
||||
export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
const { minterMessages, sg721Messages, txSigner } = args
|
||||
if (!minterMessages || !sg721Messages) {
|
||||
const { vendingMinterMessages, sg721Messages, txSigner } = args
|
||||
if (!vendingMinterMessages || !sg721Messages) {
|
||||
throw new Error('Cannot execute actions')
|
||||
}
|
||||
switch (args.type) {
|
||||
case 'mint': {
|
||||
return minterMessages.mint(txSigner)
|
||||
return vendingMinterMessages.mint(txSigner)
|
||||
}
|
||||
case 'purge': {
|
||||
return minterMessages.purge(txSigner)
|
||||
return vendingMinterMessages.purge(txSigner)
|
||||
}
|
||||
case 'update_mint_price': {
|
||||
return minterMessages.updateMintPrice(txSigner, args.price)
|
||||
return vendingMinterMessages.updateMintPrice(txSigner, args.price)
|
||||
}
|
||||
case 'mint_to': {
|
||||
return minterMessages.mintTo(txSigner, args.recipient)
|
||||
return vendingMinterMessages.mintTo(txSigner, args.recipient)
|
||||
}
|
||||
case 'mint_for': {
|
||||
return minterMessages.mintFor(txSigner, args.recipient, args.tokenId)
|
||||
return vendingMinterMessages.mintFor(txSigner, args.recipient, args.tokenId)
|
||||
}
|
||||
case 'batch_mint': {
|
||||
return minterMessages.batchMint(txSigner, args.recipient, args.batchNumber)
|
||||
return vendingMinterMessages.batchMint(txSigner, args.recipient, args.batchNumber)
|
||||
}
|
||||
case 'set_whitelist': {
|
||||
return minterMessages.setWhitelist(txSigner, args.whitelist)
|
||||
return vendingMinterMessages.setWhitelist(txSigner, args.whitelist)
|
||||
}
|
||||
case 'update_start_time': {
|
||||
return minterMessages.updateStartTime(txSigner, args.startTime)
|
||||
return vendingMinterMessages.updateStartTime(txSigner, args.startTime)
|
||||
}
|
||||
case 'update_start_trading_time': {
|
||||
return minterMessages.updateStartTradingTime(txSigner, args.startTime)
|
||||
return vendingMinterMessages.updateStartTradingTime(txSigner, args.startTime)
|
||||
}
|
||||
case 'update_per_address_limit': {
|
||||
return minterMessages.updatePerAddressLimit(txSigner, args.limit)
|
||||
return vendingMinterMessages.updatePerAddressLimit(txSigner, args.limit)
|
||||
}
|
||||
case 'update_collection_info': {
|
||||
return sg721Messages.updateCollectionInfo(args.collectionInfo as CollectionInfo)
|
||||
@@ -225,10 +225,10 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
return sg721Messages.freezeCollectionInfo()
|
||||
}
|
||||
case 'shuffle': {
|
||||
return minterMessages.shuffle(txSigner)
|
||||
return vendingMinterMessages.shuffle(txSigner)
|
||||
}
|
||||
case 'withdraw': {
|
||||
return minterMessages.withdraw(txSigner)
|
||||
return vendingMinterMessages.withdraw(txSigner)
|
||||
}
|
||||
case 'transfer': {
|
||||
return sg721Messages.transferNft(args.recipient, args.tokenId.toString())
|
||||
@@ -243,13 +243,13 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
return sg721Messages.batchBurn(args.tokenIds)
|
||||
}
|
||||
case 'batch_mint_for': {
|
||||
return minterMessages.batchMintFor(txSigner, args.recipient, args.tokenIds)
|
||||
return vendingMinterMessages.batchMintFor(txSigner, args.recipient, args.tokenIds)
|
||||
}
|
||||
case 'airdrop': {
|
||||
return minterMessages.airdrop(txSigner, args.recipients)
|
||||
return vendingMinterMessages.airdrop(txSigner, args.recipients)
|
||||
}
|
||||
case 'burn_remaining': {
|
||||
return minterMessages.burnRemaining(txSigner)
|
||||
return vendingMinterMessages.burnRemaining(txSigner)
|
||||
}
|
||||
default: {
|
||||
throw new Error('Unknown action')
|
||||
@@ -259,40 +259,40 @@ export const dispatchExecute = async (args: DispatchExecuteArgs) => {
|
||||
|
||||
export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages: minterMessages } = useMinterContract()
|
||||
const { messages: vendingMinterMessages } = useVendingMinterContract()
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { messages: sg721Messages } = useSG721Contract()
|
||||
const { minterContract, sg721Contract } = args
|
||||
switch (args.type) {
|
||||
case 'mint': {
|
||||
return minterMessages(minterContract)?.mint()
|
||||
return vendingMinterMessages(minterContract)?.mint()
|
||||
}
|
||||
case 'purge': {
|
||||
return minterMessages(minterContract)?.purge()
|
||||
return vendingMinterMessages(minterContract)?.purge()
|
||||
}
|
||||
case 'update_mint_price': {
|
||||
return minterMessages(minterContract)?.updateMintPrice(args.price)
|
||||
return vendingMinterMessages(minterContract)?.updateMintPrice(args.price)
|
||||
}
|
||||
case 'mint_to': {
|
||||
return minterMessages(minterContract)?.mintTo(args.recipient)
|
||||
return vendingMinterMessages(minterContract)?.mintTo(args.recipient)
|
||||
}
|
||||
case 'mint_for': {
|
||||
return minterMessages(minterContract)?.mintFor(args.recipient, args.tokenId)
|
||||
return vendingMinterMessages(minterContract)?.mintFor(args.recipient, args.tokenId)
|
||||
}
|
||||
case 'batch_mint': {
|
||||
return minterMessages(minterContract)?.batchMint(args.recipient, args.batchNumber)
|
||||
return vendingMinterMessages(minterContract)?.batchMint(args.recipient, args.batchNumber)
|
||||
}
|
||||
case 'set_whitelist': {
|
||||
return minterMessages(minterContract)?.setWhitelist(args.whitelist)
|
||||
return vendingMinterMessages(minterContract)?.setWhitelist(args.whitelist)
|
||||
}
|
||||
case 'update_start_time': {
|
||||
return minterMessages(minterContract)?.updateStartTime(args.startTime)
|
||||
return vendingMinterMessages(minterContract)?.updateStartTime(args.startTime)
|
||||
}
|
||||
case 'update_start_trading_time': {
|
||||
return minterMessages(minterContract)?.updateStartTradingTime(args.startTime as string)
|
||||
return vendingMinterMessages(minterContract)?.updateStartTradingTime(args.startTime as string)
|
||||
}
|
||||
case 'update_per_address_limit': {
|
||||
return minterMessages(minterContract)?.updatePerAddressLimit(args.limit)
|
||||
return vendingMinterMessages(minterContract)?.updatePerAddressLimit(args.limit)
|
||||
}
|
||||
case 'update_collection_info': {
|
||||
return sg721Messages(sg721Contract)?.updateCollectionInfo(args.collectionInfo as CollectionInfo)
|
||||
@@ -301,10 +301,10 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
return sg721Messages(sg721Contract)?.freezeCollectionInfo()
|
||||
}
|
||||
case 'shuffle': {
|
||||
return minterMessages(minterContract)?.shuffle()
|
||||
return vendingMinterMessages(minterContract)?.shuffle()
|
||||
}
|
||||
case 'withdraw': {
|
||||
return minterMessages(minterContract)?.withdraw()
|
||||
return vendingMinterMessages(minterContract)?.withdraw()
|
||||
}
|
||||
case 'transfer': {
|
||||
return sg721Messages(sg721Contract)?.transferNft(args.recipient, args.tokenId.toString())
|
||||
@@ -319,13 +319,13 @@ export const previewExecutePayload = (args: DispatchExecuteArgs) => {
|
||||
return sg721Messages(sg721Contract)?.batchBurn(args.tokenIds)
|
||||
}
|
||||
case 'batch_mint_for': {
|
||||
return minterMessages(minterContract)?.batchMintFor(args.recipient, args.tokenIds)
|
||||
return vendingMinterMessages(minterContract)?.batchMintFor(args.recipient, args.tokenIds)
|
||||
}
|
||||
case 'airdrop': {
|
||||
return minterMessages(minterContract)?.airdrop(args.recipients)
|
||||
return vendingMinterMessages(minterContract)?.airdrop(args.recipients)
|
||||
}
|
||||
case 'burn_remaining': {
|
||||
return minterMessages(minterContract)?.burnRemaining()
|
||||
return vendingMinterMessages(minterContract)?.burnRemaining()
|
||||
}
|
||||
default: {
|
||||
return {}
|
||||
|
||||
@@ -5,8 +5,8 @@ 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 type { MinterInstance } from 'contracts/vendingMinter'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { useQuery } from 'react-query'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MinterInstance } from 'contracts/minter'
|
||||
import type { SG721Instance } from 'contracts/sg721'
|
||||
import type { VendingMinterInstance } from 'contracts/vendingMinter'
|
||||
|
||||
export type QueryType = typeof QUERY_TYPES[number]
|
||||
|
||||
@@ -59,7 +59,7 @@ export interface DispatchExecuteProps {
|
||||
type Select<T extends QueryType> = T
|
||||
|
||||
export type DispatchQueryArgs = {
|
||||
minterMessages?: MinterInstance
|
||||
vendingMinterMessages?: VendingMinterInstance
|
||||
sg721Messages?: SG721Instance
|
||||
} & (
|
||||
| { type: undefined }
|
||||
@@ -72,8 +72,8 @@ export type DispatchQueryArgs = {
|
||||
)
|
||||
|
||||
export const dispatchQuery = async (args: DispatchQueryArgs) => {
|
||||
const { minterMessages, sg721Messages } = args
|
||||
if (!minterMessages || !sg721Messages) {
|
||||
const { vendingMinterMessages, sg721Messages } = args
|
||||
if (!vendingMinterMessages || !sg721Messages) {
|
||||
throw new Error('Cannot execute actions')
|
||||
}
|
||||
switch (args.type) {
|
||||
@@ -81,16 +81,16 @@ export const dispatchQuery = async (args: DispatchQueryArgs) => {
|
||||
return sg721Messages.collectionInfo()
|
||||
}
|
||||
case 'mint_price': {
|
||||
return minterMessages.getMintPrice()
|
||||
return vendingMinterMessages.getMintPrice()
|
||||
}
|
||||
case 'num_tokens': {
|
||||
return minterMessages.getMintableNumTokens()
|
||||
return vendingMinterMessages.getMintableNumTokens()
|
||||
}
|
||||
case 'tokens_minted_to_user': {
|
||||
return minterMessages.getMintCount(args.address)
|
||||
return vendingMinterMessages.getMintCount(args.address)
|
||||
}
|
||||
// case 'token_owners': {
|
||||
// return minterMessages.updateStartTime(txSigner, args.startTime)
|
||||
// return vendingMinterMessages.updateStartTime(txSigner, args.startTime)
|
||||
// }
|
||||
case 'token_info': {
|
||||
if (!args.tokenId) return
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { ExecuteListItem } from 'contracts/minter/messages/execute'
|
||||
import type { ExecuteListItem } from 'contracts/baseMinter/messages/execute'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const useExecuteComboboxState = () => {
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { Combobox, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import type { ExecuteListItem } from 'contracts/minter/messages/execute'
|
||||
import { EXECUTE_LIST } from 'contracts/minter/messages/execute'
|
||||
import type { ExecuteListItem } from 'contracts/baseMinter/messages/execute'
|
||||
import { EXECUTE_LIST } from 'contracts/baseMinter/messages/execute'
|
||||
import { matchSorter } from 'match-sorter'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { FaChevronDown, FaInfoCircle } from 'react-icons/fa'
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ExecuteListItem } from 'contracts/vendingMinter/messages/execute'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const useExecuteComboboxState = () => {
|
||||
const [value, setValue] = useState<ExecuteListItem | null>(null)
|
||||
return { value, onChange: (item: ExecuteListItem) => setValue(item) }
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Combobox, Transition } from '@headlessui/react'
|
||||
import clsx from 'clsx'
|
||||
import { FormControl } from 'components/FormControl'
|
||||
import type { ExecuteListItem } from 'contracts/vendingMinter/messages/execute'
|
||||
import { EXECUTE_LIST } from 'contracts/vendingMinter/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-plumbus-70': 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user