203 lines
5.0 KiB
TypeScript
203 lines
5.0 KiB
TypeScript
import React from "react";
|
|
import { TransactionSignature, Connection } from "@solana/web3.js";
|
|
import { findGetParameter, findPathSegment } from "../utils";
|
|
import { useCluster } from "../providers/cluster";
|
|
|
|
export enum Status {
|
|
Checking,
|
|
CheckFailed,
|
|
Success,
|
|
Failure,
|
|
Missing
|
|
}
|
|
|
|
enum Source {
|
|
Url,
|
|
Input
|
|
}
|
|
|
|
export interface Transaction {
|
|
id: number;
|
|
status: Status;
|
|
source: Source;
|
|
slot?: number;
|
|
signature: TransactionSignature;
|
|
}
|
|
|
|
type Transactions = { [id: number]: Transaction };
|
|
interface State {
|
|
idCounter: number;
|
|
transactions: Transactions;
|
|
}
|
|
|
|
export enum ActionType {
|
|
UpdateStatus,
|
|
InputSignature
|
|
}
|
|
|
|
interface UpdateStatus {
|
|
type: ActionType.UpdateStatus;
|
|
id: number;
|
|
status: Status;
|
|
slot?: number;
|
|
}
|
|
|
|
interface InputSignature {
|
|
type: ActionType.InputSignature;
|
|
signature: TransactionSignature;
|
|
}
|
|
|
|
type Action = UpdateStatus | InputSignature;
|
|
type Dispatch = (action: Action) => void;
|
|
|
|
function reducer(state: State, action: Action): State {
|
|
switch (action.type) {
|
|
case ActionType.InputSignature: {
|
|
const idCounter = state.idCounter + 1;
|
|
const transactions = {
|
|
...state.transactions,
|
|
[idCounter]: {
|
|
id: idCounter,
|
|
status: Status.Checking,
|
|
source: Source.Input,
|
|
signature: action.signature
|
|
}
|
|
};
|
|
return { ...state, transactions, idCounter };
|
|
}
|
|
case ActionType.UpdateStatus: {
|
|
let transaction = state.transactions[action.id];
|
|
if (transaction) {
|
|
transaction = {
|
|
...transaction,
|
|
status: action.status,
|
|
slot: action.slot
|
|
};
|
|
const transactions = {
|
|
...state.transactions,
|
|
[action.id]: transaction
|
|
};
|
|
return { ...state, transactions };
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return state;
|
|
}
|
|
|
|
function urlSignatures(): Array<string> {
|
|
const signatures: Array<string> = [];
|
|
return signatures
|
|
.concat(findGetParameter("tx")?.split(",") || [])
|
|
.concat(findGetParameter("txn")?.split(",") || [])
|
|
.concat(findGetParameter("txs")?.split(",") || [])
|
|
.concat(findGetParameter("txns")?.split(",") || [])
|
|
.concat(findGetParameter("transaction")?.split(",") || [])
|
|
.concat(findGetParameter("transactions")?.split(",") || [])
|
|
.concat(findPathSegment("transaction")?.split(",") || [])
|
|
.concat(findPathSegment("transactions")?.split(",") || []);
|
|
}
|
|
|
|
function initState(): State {
|
|
let idCounter = 0;
|
|
const signatures = urlSignatures();
|
|
const transactions = signatures.reduce(
|
|
(transactions: Transactions, signature) => {
|
|
const id = ++idCounter;
|
|
transactions[id] = {
|
|
id,
|
|
status: Status.Checking,
|
|
source: Source.Url,
|
|
signature
|
|
};
|
|
return transactions;
|
|
},
|
|
{}
|
|
);
|
|
return { idCounter, transactions };
|
|
}
|
|
|
|
const StateContext = React.createContext<State | undefined>(undefined);
|
|
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
|
|
|
type TransactionsProviderProps = { children: React.ReactNode };
|
|
export function TransactionsProvider({ children }: TransactionsProviderProps) {
|
|
const [state, dispatch] = React.useReducer(reducer, undefined, initState);
|
|
|
|
const { status, url } = useCluster();
|
|
|
|
// Check transaction statuses on startup and whenever cluster updates
|
|
React.useEffect(() => {
|
|
Object.values(state.transactions).forEach(tx => {
|
|
checkTransactionStatus(dispatch, tx.id, tx.signature, url);
|
|
});
|
|
}, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
return (
|
|
<StateContext.Provider value={state}>
|
|
<DispatchContext.Provider value={dispatch}>
|
|
{children}
|
|
</DispatchContext.Provider>
|
|
</StateContext.Provider>
|
|
);
|
|
}
|
|
|
|
export async function checkTransactionStatus(
|
|
dispatch: Dispatch,
|
|
id: number,
|
|
signature: TransactionSignature,
|
|
url: string
|
|
) {
|
|
dispatch({
|
|
type: ActionType.UpdateStatus,
|
|
status: Status.Checking,
|
|
id
|
|
});
|
|
|
|
let status;
|
|
let slot;
|
|
try {
|
|
const result = await new Connection(url).getSignatureStatus(signature);
|
|
|
|
if (result === null) {
|
|
status = Status.Missing;
|
|
} else {
|
|
slot = result.slot;
|
|
if ("Ok" in result.status) {
|
|
status = Status.Success;
|
|
} else {
|
|
status = Status.Failure;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to check transaction status", error);
|
|
status = Status.CheckFailed;
|
|
}
|
|
dispatch({ type: ActionType.UpdateStatus, status, slot, id });
|
|
}
|
|
|
|
export function useTransactions() {
|
|
const context = React.useContext(StateContext);
|
|
if (!context) {
|
|
throw new Error(
|
|
`useTransactions must be used within a TransactionsProvider`
|
|
);
|
|
}
|
|
return {
|
|
idCounter: context.idCounter,
|
|
transactions: Object.values(context.transactions).sort((a, b) =>
|
|
a.id <= b.id ? 1 : -1
|
|
)
|
|
};
|
|
}
|
|
|
|
export function useTransactionsDispatch() {
|
|
const context = React.useContext(DispatchContext);
|
|
if (!context) {
|
|
throw new Error(
|
|
`useTransactionsDispatch must be used within a TransactionsProvider`
|
|
);
|
|
}
|
|
return context;
|
|
}
|