Files
solana/explorer/src/providers/transactions.tsx

316 lines
7.5 KiB
TypeScript
Raw Normal View History

import React from "react";
import {
TransactionSignature,
Connection,
SystemProgram,
Account
} from "@solana/web3.js";
import { findGetParameter, findPathSegment } from "../utils";
2020-03-26 22:35:02 +08:00
import { useCluster, ClusterStatus } from "../providers/cluster";
import base58 from "bs58";
export enum Status {
Checking,
CheckFailed,
Success,
Failure,
2020-03-19 22:31:05 +08:00
Missing
}
enum Source {
Url,
Input
}
2020-03-26 22:35:02 +08:00
export type Confirmations = number | "max";
export interface Transaction {
id: number;
status: Status;
2020-03-19 22:31:05 +08:00
source: Source;
slot?: number;
2020-03-26 22:35:02 +08:00
confirmations?: Confirmations;
signature: TransactionSignature;
}
2020-04-01 19:40:27 +08:00
export interface Selected {
slot: number;
signature: TransactionSignature;
}
type Transactions = { [signature: string]: Transaction };
interface State {
idCounter: number;
2020-04-01 19:40:27 +08:00
selected?: Selected;
transactions: Transactions;
}
2020-03-19 22:31:05 +08:00
export enum ActionType {
UpdateStatus,
2020-04-01 19:40:27 +08:00
InputSignature,
Select,
Deselect
}
interface SelectTransaction {
type: ActionType.Select;
signature: TransactionSignature;
}
interface DeselectTransaction {
type: ActionType.Deselect;
2020-03-19 22:31:05 +08:00
}
interface UpdateStatus {
2020-03-19 22:31:05 +08:00
type: ActionType.UpdateStatus;
signature: TransactionSignature;
status: Status;
slot?: number;
2020-03-26 22:35:02 +08:00
confirmations?: Confirmations;
}
2020-03-19 22:31:05 +08:00
interface InputSignature {
type: ActionType.InputSignature;
signature: TransactionSignature;
}
2020-04-01 19:40:27 +08:00
type Action =
| UpdateStatus
| InputSignature
| SelectTransaction
| DeselectTransaction;
type Dispatch = (action: Action) => void;
function reducer(state: State, action: Action): State {
2020-03-19 22:31:05 +08:00
switch (action.type) {
2020-04-01 19:40:27 +08:00
case ActionType.Deselect: {
return { ...state, selected: undefined };
}
case ActionType.Select: {
const tx = state.transactions[action.signature];
if (!tx.slot) return state;
const selected = {
slot: tx.slot,
signature: tx.signature
};
return { ...state, selected };
}
2020-03-19 22:31:05 +08:00
case ActionType.InputSignature: {
if (!!state.transactions[action.signature]) return state;
2020-03-19 22:31:05 +08:00
const idCounter = state.idCounter + 1;
const transactions = {
...state.transactions,
[action.signature]: {
2020-03-19 22:31:05 +08:00
id: idCounter,
status: Status.Checking,
source: Source.Input,
signature: action.signature
}
};
return { ...state, transactions, idCounter };
}
case ActionType.UpdateStatus: {
let transaction = state.transactions[action.signature];
2020-03-19 22:31:05 +08:00
if (transaction) {
transaction = {
...transaction,
status: action.status,
2020-03-26 22:35:02 +08:00
slot: action.slot,
confirmations: action.confirmations
};
2020-03-19 22:31:05 +08:00
const transactions = {
...state.transactions,
[action.signature]: transaction
2020-03-19 22:31:05 +08:00
};
return { ...state, transactions };
}
break;
}
}
return state;
}
export const TX_PATHS = [
"tx",
"txs",
"txn",
"txns",
"transaction",
"transactions"
];
function urlSignatures(): Array<string> {
const signatures: Array<string> = [];
TX_PATHS.forEach(path => {
const params = findGetParameter(path)?.split(",") || [];
const segments = findPathSegment(path)?.split(",") || [];
signatures.push(...params);
signatures.push(...segments);
});
return signatures.filter(s => s.length > 0);
}
function initState(): State {
let idCounter = 0;
const signatures = urlSignatures();
const transactions = signatures.reduce(
(transactions: Transactions, signature) => {
if (!!transactions[signature]) return transactions;
idCounter++;
transactions[signature] = {
id: idCounter,
signature,
status: Status.Checking,
source: Source.Url
};
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);
2020-03-31 14:36:40 +08:00
const { status, url } = useCluster();
2020-03-31 14:36:40 +08:00
// Check transaction statuses on startup and whenever cluster updates
React.useEffect(() => {
2020-03-26 22:35:02 +08:00
if (status !== ClusterStatus.Connected) return;
// Create a test transaction
if (findGetParameter("test") !== null) {
createTestTransaction(dispatch, url);
2020-03-26 22:35:02 +08:00
}
Object.keys(state.transactions).forEach(signature => {
checkTransactionStatus(dispatch, 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>
);
}
async function createTestTransaction(dispatch: Dispatch, url: string) {
const testKey = process.env.REACT_APP_TEST_KEY;
let testAccount = new Account();
if (testKey) {
testAccount = new Account(base58.decode(testKey));
}
2020-03-26 22:35:02 +08:00
try {
const connection = new Connection(url, "recent");
2020-03-26 22:35:02 +08:00
const signature = await connection.requestAirdrop(
testAccount.publicKey,
100000,
2020-03-26 22:35:02 +08:00
"recent"
);
dispatch({ type: ActionType.InputSignature, signature });
checkTransactionStatus(dispatch, signature, url);
2020-03-26 22:35:02 +08:00
} catch (error) {
console.error("Failed to create test success transaction", error);
}
try {
const connection = new Connection(url, "recent");
const tx = SystemProgram.transfer({
fromPubkey: testAccount.publicKey,
toPubkey: testAccount.publicKey,
lamports: 1
});
const signature = await connection.sendTransaction(tx, testAccount);
dispatch({ type: ActionType.InputSignature, signature });
checkTransactionStatus(dispatch, signature, url);
} catch (error) {
console.error("Failed to create test failure transaction", error);
2020-03-26 22:35:02 +08:00
}
}
export async function checkTransactionStatus(
dispatch: Dispatch,
2020-03-19 22:31:05 +08:00
signature: TransactionSignature,
url: string
) {
dispatch({
2020-03-19 22:31:05 +08:00
type: ActionType.UpdateStatus,
status: Status.Checking,
signature
});
let status;
let slot;
2020-03-26 22:35:02 +08:00
let confirmations: Confirmations | undefined;
try {
const { value } = await new Connection(url, "recent").getSignatureStatus(
signature
);
2020-03-31 13:47:43 +08:00
if (value === null) {
2020-03-19 22:31:05 +08:00
status = Status.Missing;
} else {
2020-03-31 13:47:43 +08:00
slot = value.slot;
2020-03-26 22:35:02 +08:00
if (typeof value.confirmations === "number") {
confirmations = value.confirmations;
} else {
confirmations = "max";
}
2020-04-06 01:22:25 +08:00
if (value.err) {
status = Status.Failure;
2020-04-06 01:22:25 +08:00
} else {
status = Status.Success;
}
}
} catch (error) {
console.error("Failed to check transaction status", error);
status = Status.CheckFailed;
}
dispatch({
type: ActionType.UpdateStatus,
status,
slot,
confirmations,
signature
});
}
export function useTransactions() {
const context = React.useContext(StateContext);
if (!context) {
throw new Error(
`useTransactions must be used within a TransactionsProvider`
);
}
2020-03-19 22:31:05 +08:00
return {
idCounter: context.idCounter,
2020-04-01 19:40:27 +08:00
selected: context.selected,
2020-03-19 22:31:05 +08:00
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;
}