Fix data fetching races in explorer (#11468)
This commit is contained in:
@ -49,31 +49,41 @@ export function TransactionDetailsPage({ signature }: Props) {
|
|||||||
function StatusCard({ signature }: Props) {
|
function StatusCard({ signature }: Props) {
|
||||||
const fetchStatus = useFetchTransactionStatus();
|
const fetchStatus = useFetchTransactionStatus();
|
||||||
const status = useTransactionStatus(signature);
|
const status = useTransactionStatus(signature);
|
||||||
const refresh = useFetchTransactionStatus();
|
const fetchDetails = useFetchTransactionDetails();
|
||||||
const details = useTransactionDetails(signature);
|
const details = useTransactionDetails(signature);
|
||||||
const { firstAvailableBlock, status: clusterStatus } = useCluster();
|
const { firstAvailableBlock, status: clusterStatus } = useCluster();
|
||||||
|
const refresh = React.useCallback(
|
||||||
|
(signature: string) => {
|
||||||
|
fetchStatus(signature);
|
||||||
|
fetchDetails(signature);
|
||||||
|
},
|
||||||
|
[fetchStatus, fetchDetails]
|
||||||
|
);
|
||||||
|
|
||||||
// Fetch transaction on load
|
// Fetch transaction on load
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!status && clusterStatus === ClusterStatus.Connected)
|
if (!status && clusterStatus === ClusterStatus.Connected) {
|
||||||
fetchStatus(signature);
|
fetchStatus(signature);
|
||||||
|
}
|
||||||
}, [signature, clusterStatus]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [signature, clusterStatus]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
if (!status || status.fetchStatus === FetchStatus.Fetching) {
|
if (!status || status.fetchStatus === FetchStatus.Fetching) {
|
||||||
return <LoadingCard />;
|
return <LoadingCard />;
|
||||||
} else if (status?.fetchStatus === FetchStatus.FetchFailed) {
|
} else if (status?.fetchStatus === FetchStatus.FetchFailed) {
|
||||||
return <ErrorCard retry={() => refresh(signature)} text="Fetch Failed" />;
|
return (
|
||||||
|
<ErrorCard retry={() => fetchStatus(signature)} text="Fetch Failed" />
|
||||||
|
);
|
||||||
} else if (!status.info) {
|
} else if (!status.info) {
|
||||||
if (firstAvailableBlock !== undefined) {
|
if (firstAvailableBlock !== undefined) {
|
||||||
return (
|
return (
|
||||||
<ErrorCard
|
<ErrorCard
|
||||||
retry={() => refresh(signature)}
|
retry={() => fetchStatus(signature)}
|
||||||
text="Not Found"
|
text="Not Found"
|
||||||
subtext={`Note: Transactions processed before block ${firstAvailableBlock} are not available at this time`}
|
subtext={`Note: Transactions processed before block ${firstAvailableBlock} are not available at this time`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <ErrorCard retry={() => refresh(signature)} text="Not Found" />;
|
return <ErrorCard retry={() => fetchStatus(signature)} text="Not Found" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { info } = status;
|
const { info } = status;
|
||||||
@ -187,9 +197,8 @@ function StatusCard({ signature }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AccountsCard({ signature }: Props) {
|
function AccountsCard({ signature }: Props) {
|
||||||
const details = useTransactionDetails(signature);
|
|
||||||
|
|
||||||
const { url } = useCluster();
|
const { url } = useCluster();
|
||||||
|
const details = useTransactionDetails(signature);
|
||||||
const fetchStatus = useFetchTransactionStatus();
|
const fetchStatus = useFetchTransactionStatus();
|
||||||
const fetchDetails = useFetchTransactionDetails();
|
const fetchDetails = useFetchTransactionDetails();
|
||||||
const refreshStatus = () => fetchStatus(signature);
|
const refreshStatus = () => fetchStatus(signature);
|
||||||
@ -198,6 +207,13 @@ function AccountsCard({ signature }: Props) {
|
|||||||
const message = transaction?.message;
|
const message = transaction?.message;
|
||||||
const status = useTransactionStatus(signature);
|
const status = useTransactionStatus(signature);
|
||||||
|
|
||||||
|
// Fetch details on load
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (status?.info?.confirmations === "max" && !details) {
|
||||||
|
fetchDetails(signature);
|
||||||
|
}
|
||||||
|
}, [signature, details, status, fetchDetails]);
|
||||||
|
|
||||||
if (!status || !status.info) {
|
if (!status || !status.info) {
|
||||||
return null;
|
return null;
|
||||||
} else if (!details) {
|
} else if (!details) {
|
||||||
|
@ -29,6 +29,7 @@ export interface Account {
|
|||||||
type Accounts = { [address: string]: Account };
|
type Accounts = { [address: string]: Account };
|
||||||
interface State {
|
interface State {
|
||||||
accounts: Accounts;
|
accounts: Accounts;
|
||||||
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ActionType {
|
export enum ActionType {
|
||||||
@ -39,6 +40,7 @@ export enum ActionType {
|
|||||||
|
|
||||||
interface Update {
|
interface Update {
|
||||||
type: ActionType.Update;
|
type: ActionType.Update;
|
||||||
|
url: string;
|
||||||
pubkey: PublicKey;
|
pubkey: PublicKey;
|
||||||
data: {
|
data: {
|
||||||
status: FetchStatus;
|
status: FetchStatus;
|
||||||
@ -49,17 +51,25 @@ interface Update {
|
|||||||
|
|
||||||
interface Fetch {
|
interface Fetch {
|
||||||
type: ActionType.Fetch;
|
type: ActionType.Fetch;
|
||||||
|
url: string;
|
||||||
pubkey: PublicKey;
|
pubkey: PublicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Clear {
|
interface Clear {
|
||||||
type: ActionType.Clear;
|
type: ActionType.Clear;
|
||||||
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action = Update | Fetch | Clear;
|
type Action = Update | Fetch | Clear;
|
||||||
type Dispatch = (action: Action) => void;
|
type Dispatch = (action: Action) => void;
|
||||||
|
|
||||||
function reducer(state: State, action: Action): State {
|
function reducer(state: State, action: Action): State {
|
||||||
|
if (action.type === ActionType.Clear) {
|
||||||
|
return { url: action.url, accounts: {} };
|
||||||
|
} else if (action.url !== state.url) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionType.Fetch: {
|
case ActionType.Fetch: {
|
||||||
const address = action.pubkey.toBase58();
|
const address = action.pubkey.toBase58();
|
||||||
@ -100,13 +110,6 @@ function reducer(state: State, action: Action): State {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case ActionType.Clear: {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
accounts: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -116,14 +119,15 @@ const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
|||||||
|
|
||||||
type AccountsProviderProps = { children: React.ReactNode };
|
type AccountsProviderProps = { children: React.ReactNode };
|
||||||
export function AccountsProvider({ children }: AccountsProviderProps) {
|
export function AccountsProvider({ children }: AccountsProviderProps) {
|
||||||
|
const { url } = useCluster();
|
||||||
const [state, dispatch] = React.useReducer(reducer, {
|
const [state, dispatch] = React.useReducer(reducer, {
|
||||||
|
url,
|
||||||
accounts: {},
|
accounts: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear account statuses whenever cluster is changed
|
// Clear account statuses whenever cluster is changed
|
||||||
const { url } = useCluster();
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
dispatch({ type: ActionType.Clear });
|
dispatch({ type: ActionType.Clear, url });
|
||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -145,6 +149,7 @@ async function fetchAccountInfo(
|
|||||||
dispatch({
|
dispatch({
|
||||||
type: ActionType.Fetch,
|
type: ActionType.Fetch,
|
||||||
pubkey,
|
pubkey,
|
||||||
|
url,
|
||||||
});
|
});
|
||||||
|
|
||||||
let fetchStatus;
|
let fetchStatus;
|
||||||
@ -182,7 +187,7 @@ async function fetchAccountInfo(
|
|||||||
fetchStatus = FetchStatus.FetchFailed;
|
fetchStatus = FetchStatus.FetchFailed;
|
||||||
}
|
}
|
||||||
const data = { status: fetchStatus, lamports, details };
|
const data = { status: fetchStatus, lamports, details };
|
||||||
dispatch({ type: ActionType.Update, data, pubkey });
|
dispatch({ type: ActionType.Update, data, pubkey, url });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAccounts() {
|
export function useAccounts() {
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
ParsedConfirmedTransaction,
|
ParsedConfirmedTransaction,
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { useCluster } from "../cluster";
|
import { useCluster } from "../cluster";
|
||||||
import { useTransactions, FetchStatus } from "./index";
|
import { FetchStatus } from "./index";
|
||||||
import { CACHED_DETAILS, isCached } from "./cached";
|
import { CACHED_DETAILS, isCached } from "./cached";
|
||||||
|
|
||||||
export interface Details {
|
export interface Details {
|
||||||
@ -13,68 +13,69 @@ export interface Details {
|
|||||||
transaction: ParsedConfirmedTransaction | null;
|
transaction: ParsedConfirmedTransaction | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = { [signature: string]: Details };
|
type State = {
|
||||||
|
entries: { [signature: string]: Details };
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
export enum ActionType {
|
export enum ActionType {
|
||||||
Update,
|
Update,
|
||||||
Add,
|
|
||||||
Clear,
|
Clear,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Update {
|
interface Update {
|
||||||
type: ActionType.Update;
|
type: ActionType.Update;
|
||||||
|
url: string;
|
||||||
signature: string;
|
signature: string;
|
||||||
fetchStatus: FetchStatus;
|
fetchStatus: FetchStatus;
|
||||||
transaction: ParsedConfirmedTransaction | null;
|
transaction: ParsedConfirmedTransaction | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Add {
|
|
||||||
type: ActionType.Add;
|
|
||||||
signature: TransactionSignature;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Clear {
|
interface Clear {
|
||||||
type: ActionType.Clear;
|
type: ActionType.Clear;
|
||||||
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action = Update | Add | Clear;
|
type Action = Update | Clear;
|
||||||
type Dispatch = (action: Action) => void;
|
type Dispatch = (action: Action) => void;
|
||||||
|
|
||||||
function reducer(state: State, action: Action): State {
|
function reducer(state: State, action: Action): State {
|
||||||
switch (action.type) {
|
if (action.type === ActionType.Clear) {
|
||||||
case ActionType.Add: {
|
return { url: action.url, entries: {} };
|
||||||
const details = { ...state };
|
} else if (action.url !== state.url) {
|
||||||
const signature = action.signature;
|
return state;
|
||||||
if (!details[signature]) {
|
|
||||||
details[signature] = {
|
|
||||||
fetchStatus: FetchStatus.Fetching,
|
|
||||||
transaction: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return details;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
case ActionType.Update: {
|
case ActionType.Update: {
|
||||||
let details = state[action.signature];
|
const signature = action.signature;
|
||||||
|
const details = state.entries[signature];
|
||||||
if (details) {
|
if (details) {
|
||||||
details = {
|
return {
|
||||||
|
...state,
|
||||||
|
entries: {
|
||||||
|
...state.entries,
|
||||||
|
[signature]: {
|
||||||
...details,
|
...details,
|
||||||
fetchStatus: action.fetchStatus,
|
fetchStatus: action.fetchStatus,
|
||||||
transaction: action.transaction,
|
transaction: action.transaction,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[action.signature]: details,
|
entries: {
|
||||||
|
...state.entries,
|
||||||
|
[signature]: {
|
||||||
|
fetchStatus: FetchStatus.Fetching,
|
||||||
|
transaction: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case ActionType.Clear: {
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StateContext = React.createContext<State | undefined>(undefined);
|
export const StateContext = React.createContext<State | undefined>(undefined);
|
||||||
@ -84,28 +85,13 @@ export const DispatchContext = React.createContext<Dispatch | undefined>(
|
|||||||
|
|
||||||
type DetailsProviderProps = { children: React.ReactNode };
|
type DetailsProviderProps = { children: React.ReactNode };
|
||||||
export function DetailsProvider({ children }: DetailsProviderProps) {
|
export function DetailsProvider({ children }: DetailsProviderProps) {
|
||||||
const [state, dispatch] = React.useReducer(reducer, {});
|
|
||||||
const { transactions, lastFetched } = useTransactions();
|
|
||||||
const { url } = useCluster();
|
const { url } = useCluster();
|
||||||
|
const [state, dispatch] = React.useReducer(reducer, { url, entries: {} });
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
dispatch({ type: ActionType.Clear });
|
dispatch({ type: ActionType.Clear, url });
|
||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
// Filter blocks for current transaction slots
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (lastFetched) {
|
|
||||||
const confirmed =
|
|
||||||
transactions[lastFetched] &&
|
|
||||||
transactions[lastFetched].info?.confirmations === "max";
|
|
||||||
const noDetails = !state[lastFetched];
|
|
||||||
if (confirmed && noDetails) {
|
|
||||||
dispatch({ type: ActionType.Add, signature: lastFetched });
|
|
||||||
fetchDetails(dispatch, lastFetched, url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [transactions, lastFetched]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StateContext.Provider value={state}>
|
<StateContext.Provider value={state}>
|
||||||
<DispatchContext.Provider value={dispatch}>
|
<DispatchContext.Provider value={dispatch}>
|
||||||
@ -125,6 +111,7 @@ async function fetchDetails(
|
|||||||
fetchStatus: FetchStatus.Fetching,
|
fetchStatus: FetchStatus.Fetching,
|
||||||
transaction: null,
|
transaction: null,
|
||||||
signature,
|
signature,
|
||||||
|
url,
|
||||||
});
|
});
|
||||||
|
|
||||||
let fetchStatus;
|
let fetchStatus;
|
||||||
@ -143,7 +130,13 @@ async function fetchDetails(
|
|||||||
fetchStatus = FetchStatus.FetchFailed;
|
fetchStatus = FetchStatus.FetchFailed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dispatch({ type: ActionType.Update, fetchStatus, signature, transaction });
|
dispatch({
|
||||||
|
type: ActionType.Update,
|
||||||
|
fetchStatus,
|
||||||
|
signature,
|
||||||
|
transaction,
|
||||||
|
url,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFetchTransactionDetails() {
|
export function useFetchTransactionDetails() {
|
||||||
|
@ -44,7 +44,7 @@ export interface TransactionStatus {
|
|||||||
type Transactions = { [signature: string]: TransactionStatus };
|
type Transactions = { [signature: string]: TransactionStatus };
|
||||||
interface State {
|
interface State {
|
||||||
transactions: Transactions;
|
transactions: Transactions;
|
||||||
lastFetched: TransactionSignature | undefined;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ActionType {
|
export enum ActionType {
|
||||||
@ -55,6 +55,7 @@ export enum ActionType {
|
|||||||
|
|
||||||
interface UpdateStatus {
|
interface UpdateStatus {
|
||||||
type: ActionType.UpdateStatus;
|
type: ActionType.UpdateStatus;
|
||||||
|
url: string;
|
||||||
signature: TransactionSignature;
|
signature: TransactionSignature;
|
||||||
fetchStatus: FetchStatus;
|
fetchStatus: FetchStatus;
|
||||||
info?: TransactionStatusInfo;
|
info?: TransactionStatusInfo;
|
||||||
@ -62,17 +63,25 @@ interface UpdateStatus {
|
|||||||
|
|
||||||
interface FetchSignature {
|
interface FetchSignature {
|
||||||
type: ActionType.FetchSignature;
|
type: ActionType.FetchSignature;
|
||||||
|
url: string;
|
||||||
signature: TransactionSignature;
|
signature: TransactionSignature;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Clear {
|
interface Clear {
|
||||||
type: ActionType.Clear;
|
type: ActionType.Clear;
|
||||||
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action = UpdateStatus | FetchSignature | Clear;
|
type Action = UpdateStatus | FetchSignature | Clear;
|
||||||
type Dispatch = (action: Action) => void;
|
type Dispatch = (action: Action) => void;
|
||||||
|
|
||||||
function reducer(state: State, action: Action): State {
|
function reducer(state: State, action: Action): State {
|
||||||
|
if (action.type === ActionType.Clear) {
|
||||||
|
return { url: action.url, transactions: {} };
|
||||||
|
} else if (action.url !== state.url) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionType.FetchSignature: {
|
case ActionType.FetchSignature: {
|
||||||
const signature = action.signature;
|
const signature = action.signature;
|
||||||
@ -86,7 +95,7 @@ function reducer(state: State, action: Action): State {
|
|||||||
info: undefined,
|
info: undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return { ...state, transactions, lastFetched: signature };
|
return { ...state, transactions };
|
||||||
} else {
|
} else {
|
||||||
const transactions = {
|
const transactions = {
|
||||||
...state.transactions,
|
...state.transactions,
|
||||||
@ -95,7 +104,7 @@ function reducer(state: State, action: Action): State {
|
|||||||
fetchStatus: FetchStatus.Fetching,
|
fetchStatus: FetchStatus.Fetching,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return { ...state, transactions, lastFetched: signature };
|
return { ...state, transactions };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,13 +123,6 @@ function reducer(state: State, action: Action): State {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case ActionType.Clear: {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
transactions: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -132,12 +134,12 @@ const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
|||||||
|
|
||||||
type TransactionsProviderProps = { children: React.ReactNode };
|
type TransactionsProviderProps = { children: React.ReactNode };
|
||||||
export function TransactionsProvider({ children }: TransactionsProviderProps) {
|
export function TransactionsProvider({ children }: TransactionsProviderProps) {
|
||||||
|
const { cluster, status: clusterStatus, url } = useCluster();
|
||||||
const [state, dispatch] = React.useReducer(reducer, {
|
const [state, dispatch] = React.useReducer(reducer, {
|
||||||
transactions: {},
|
transactions: {},
|
||||||
lastFetched: undefined,
|
url,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { cluster, status: clusterStatus, url } = useCluster();
|
|
||||||
const fetchAccount = useFetchAccountInfo();
|
const fetchAccount = useFetchAccountInfo();
|
||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
const testFlag = query.get("test");
|
const testFlag = query.get("test");
|
||||||
@ -145,9 +147,7 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
|
|||||||
// Check transaction statuses whenever cluster updates
|
// Check transaction statuses whenever cluster updates
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (clusterStatus === ClusterStatus.Connecting) {
|
if (clusterStatus === ClusterStatus.Connecting) {
|
||||||
dispatch({ type: ActionType.Clear });
|
dispatch({ type: ActionType.Clear, url });
|
||||||
} else if (clusterStatus === ClusterStatus.Connected && state.lastFetched) {
|
|
||||||
fetchTransactionStatus(dispatch, state.lastFetched, url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a test transaction
|
// Create a test transaction
|
||||||
@ -216,6 +216,7 @@ export async function fetchTransactionStatus(
|
|||||||
dispatch({
|
dispatch({
|
||||||
type: ActionType.FetchSignature,
|
type: ActionType.FetchSignature,
|
||||||
signature,
|
signature,
|
||||||
|
url,
|
||||||
});
|
});
|
||||||
|
|
||||||
let fetchStatus;
|
let fetchStatus;
|
||||||
@ -261,6 +262,7 @@ export async function fetchTransactionStatus(
|
|||||||
signature,
|
signature,
|
||||||
fetchStatus,
|
fetchStatus,
|
||||||
info,
|
info,
|
||||||
|
url,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,7 +297,7 @@ export function useTransactionDetails(signature: TransactionSignature) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return context[signature];
|
return context.entries[signature];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFetchTransactionStatus() {
|
export function useFetchTransactionStatus() {
|
||||||
|
Reference in New Issue
Block a user