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

349 lines
8.7 KiB
TypeScript
Raw Normal View History

import React from "react";
import {
TransactionSignature,
Connection,
SystemProgram,
2020-04-29 11:00:06 +08:00
Account,
SignatureResult
} from "@solana/web3.js";
import { findGetParameter, findPathSegment } from "../../utils/url";
import { useCluster, ClusterStatus } from "../cluster";
import {
DetailsProvider,
StateContext as DetailsStateContext,
DispatchContext as DetailsDispatchContext
} from "./details";
import base58 from "bs58";
import {
useAccountsDispatch,
fetchAccountInfo,
Dispatch as AccountsDispatch,
ActionType as AccountsActionType
} from "../accounts";
2020-04-29 11:00:06 +08:00
export enum FetchStatus {
Fetching,
FetchFailed,
Fetched
2020-03-19 22:31:05 +08:00
}
enum Source {
Url,
Input
}
2020-03-26 22:35:02 +08:00
export type Confirmations = number | "max";
2020-04-29 11:00:06 +08:00
export interface TransactionStatus {
slot: number;
result: SignatureResult;
confirmations: Confirmations;
}
2020-04-29 11:00:06 +08:00
export interface TransactionState {
id: number;
source: Source;
fetchStatus: FetchStatus;
2020-04-01 19:40:27 +08:00
signature: TransactionSignature;
2020-04-29 11:00:06 +08:00
transactionStatus?: TransactionStatus;
2020-04-01 19:40:27 +08:00
}
2020-04-29 11:00:06 +08:00
type Transactions = { [signature: string]: TransactionState };
interface State {
idCounter: number;
2020-04-29 11:00:06 +08:00
selected?: TransactionSignature;
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;
2020-04-29 11:00:06 +08:00
fetchStatus: FetchStatus;
transactionStatus?: TransactionStatus;
}
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;
2020-04-29 11:00:06 +08:00
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];
2020-04-29 11:00:06 +08:00
return { ...state, selected: tx.signature };
2020-04-01 19:40:27 +08:00
}
2020-03-19 22:31:05 +08:00
case ActionType.InputSignature: {
if (!!state.transactions[action.signature]) return state;
2020-04-29 11:00:06 +08:00
const nextId = state.idCounter + 1;
2020-03-19 22:31:05 +08:00
const transactions = {
...state.transactions,
[action.signature]: {
2020-04-29 11:00:06 +08:00
id: nextId,
2020-03-19 22:31:05 +08:00
source: Source.Input,
2020-04-29 11:00:06 +08:00
signature: action.signature,
fetchStatus: FetchStatus.Fetching
2020-03-19 22:31:05 +08:00
}
};
2020-04-29 11:00:06 +08:00
return { ...state, transactions, idCounter: nextId };
2020-03-19 22:31:05 +08:00
}
case ActionType.UpdateStatus: {
let transaction = state.transactions[action.signature];
2020-03-19 22:31:05 +08:00
if (transaction) {
transaction = {
...transaction,
2020-04-29 11:00:06 +08:00
fetchStatus: action.fetchStatus
};
2020-04-29 11:00:06 +08:00
if (action.transactionStatus) {
transaction.transactionStatus = action.transactionStatus;
}
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 name = path.slice(1);
const params = findGetParameter(name)?.split(",") || [];
const segments = findPathSegment(name)?.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;
2020-04-29 11:00:06 +08:00
const nextId = idCounter + 1;
transactions[signature] = {
2020-04-29 11:00:06 +08:00
id: nextId,
source: Source.Url,
signature,
2020-04-29 11:00:06 +08:00
fetchStatus: FetchStatus.Fetching
};
2020-04-29 11:00:06 +08:00
idCounter++;
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();
const accountsDispatch = useAccountsDispatch();
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, accountsDispatch, 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}>
<DetailsProvider>{children}</DetailsProvider>
</DispatchContext.Provider>
</StateContext.Provider>
);
}
async function createTestTransaction(
dispatch: Dispatch,
accountsDispatch: AccountsDispatch,
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);
accountsDispatch({
type: AccountsActionType.Input,
pubkey: testAccount.publicKey
});
fetchAccountInfo(accountsDispatch, testAccount.publicKey.toBase58(), 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,
2020-04-29 11:00:06 +08:00
signature,
fetchStatus: FetchStatus.Fetching
});
2020-04-29 11:00:06 +08:00
let fetchStatus;
let transactionStatus: TransactionStatus | undefined;
try {
2020-04-07 00:30:34 +08:00
const { value } = await new Connection(url).getSignatureStatus(signature, {
searchTransactionHistory: true
});
2020-04-29 11:00:06 +08:00
if (value !== null) {
let confirmations: Confirmations;
2020-03-26 22:35:02 +08:00
if (typeof value.confirmations === "number") {
confirmations = value.confirmations;
} else {
confirmations = "max";
}
2020-04-29 11:00:06 +08:00
transactionStatus = {
slot: value.slot,
confirmations,
result: { err: value.err }
};
}
2020-04-29 11:00:06 +08:00
fetchStatus = FetchStatus.Fetched;
} catch (error) {
2020-04-29 11:00:06 +08:00
console.error("Failed to fetch transaction status", error);
fetchStatus = FetchStatus.FetchFailed;
}
dispatch({
type: ActionType.UpdateStatus,
2020-04-29 11:00:06 +08:00
signature,
fetchStatus,
transactionStatus
});
}
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;
}
export function useDetailsDispatch() {
const context = React.useContext(DetailsDispatchContext);
if (!context) {
throw new Error(
`useDetailsDispatch must be used within a TransactionsProvider`
);
}
return context;
}
export function useDetails(signature: TransactionSignature) {
const context = React.useContext(DetailsStateContext);
if (!context) {
throw new Error(`useDetails must be used within a TransactionsProvider`);
}
return context[signature];
}