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

334 lines
8.6 KiB
TypeScript
Raw Normal View History

import React from "react";
import {
TransactionSignature,
Connection,
SystemProgram,
2020-04-29 11:00:06 +08:00
Account,
2020-05-12 18:32:14 +08:00
SignatureResult,
PublicKey,
sendAndConfirmTransaction
} from "@solana/web3.js";
2020-05-02 12:55:12 +08:00
import { useQuery } from "../../utils/url";
2020-05-07 16:41:42 +08:00
import { useCluster, Cluster, ClusterStatus } from "../cluster";
import {
DetailsProvider,
StateContext as DetailsStateContext,
DispatchContext as DetailsDispatchContext
} from "./details";
import base58 from "bs58";
2020-05-12 18:32:14 +08:00
import { useFetchAccountInfo } from "../accounts";
2020-04-29 11:00:06 +08:00
export enum FetchStatus {
Fetching,
FetchFailed,
Fetched
2020-03-19 22:31:05 +08:00
}
2020-03-26 22:35:02 +08:00
export type Confirmations = number | "max";
2020-05-23 16:11:16 +08:00
export type Timestamp = number | "unavailable";
2020-04-29 20:48:38 +08:00
export interface TransactionStatusInfo {
2020-04-29 11:00:06 +08:00
slot: number;
result: SignatureResult;
2020-05-23 16:11:16 +08:00
timestamp: Timestamp;
2020-04-29 11:00:06 +08:00
confirmations: Confirmations;
}
2020-04-29 20:48:38 +08:00
export interface TransactionStatus {
2020-04-29 11:00:06 +08:00
id: number;
fetchStatus: FetchStatus;
2020-04-01 19:40:27 +08:00
signature: TransactionSignature;
2020-04-29 20:48:38 +08:00
info?: TransactionStatusInfo;
2020-04-01 19:40:27 +08:00
}
2020-04-29 20:48:38 +08:00
type Transactions = { [signature: string]: TransactionStatus };
interface State {
idCounter: number;
transactions: Transactions;
}
2020-03-19 22:31:05 +08:00
export enum ActionType {
UpdateStatus,
2020-04-29 20:48:38 +08:00
FetchSignature
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;
2020-04-29 20:48:38 +08:00
info?: TransactionStatusInfo;
}
2020-04-29 20:48:38 +08:00
interface FetchSignature {
type: ActionType.FetchSignature;
2020-03-19 22:31:05 +08:00
signature: TransactionSignature;
}
2020-04-29 20:48:38 +08:00
type Action = UpdateStatus | FetchSignature;
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-29 20:48:38 +08:00
case ActionType.FetchSignature: {
2020-05-12 18:32:14 +08:00
const transaction = state.transactions[action.signature];
if (transaction) {
const transactions = {
...state.transactions,
[action.signature]: {
...transaction,
fetchStatus: FetchStatus.Fetching,
info: undefined
}
};
return { ...state, transactions };
} else {
const nextId = state.idCounter + 1;
const transactions = {
...state.transactions,
[action.signature]: {
id: nextId,
signature: action.signature,
fetchStatus: FetchStatus.Fetching
}
};
return { ...state, transactions, idCounter: nextId };
}
2020-03-19 22:31:05 +08:00
}
2020-05-12 18:32:14 +08:00
2020-03-19 22:31:05 +08:00
case ActionType.UpdateStatus: {
2020-05-12 18:32:14 +08:00
const transaction = state.transactions[action.signature];
2020-03-19 22:31:05 +08:00
if (transaction) {
const transactions = {
...state.transactions,
2020-05-12 18:32:14 +08:00
[action.signature]: {
...transaction,
fetchStatus: action.fetchStatus,
info: action.info
}
2020-03-19 22:31:05 +08:00
};
return { ...state, transactions };
}
break;
}
}
return state;
}
2020-04-29 20:48:38 +08:00
export const TX_ALIASES = ["tx", "txn", "transaction"];
const StateContext = React.createContext<State | undefined>(undefined);
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
type TransactionsProviderProps = { children: React.ReactNode };
export function TransactionsProvider({ children }: TransactionsProviderProps) {
2020-04-29 20:48:38 +08:00
const [state, dispatch] = React.useReducer(reducer, {
idCounter: 0,
transactions: {}
});
2020-05-07 16:41:42 +08:00
const { cluster, status: clusterStatus, url } = useCluster();
2020-05-12 18:32:14 +08:00
const fetchAccount = useFetchAccountInfo();
2020-05-02 12:55:12 +08:00
const query = useQuery();
2020-05-02 19:34:44 +08:00
const testFlag = query.get("test");
2020-04-29 20:48:38 +08:00
// Check transaction statuses whenever cluster updates
React.useEffect(() => {
2020-04-29 20:48:38 +08:00
Object.keys(state.transactions).forEach(signature => {
2020-05-07 16:41:42 +08:00
fetchTransactionStatus(dispatch, signature, url, clusterStatus);
2020-04-29 20:48:38 +08:00
});
2020-03-26 22:35:02 +08:00
// Create a test transaction
2020-05-02 19:34:44 +08:00
if (cluster === Cluster.Devnet && testFlag !== null) {
2020-05-12 18:32:14 +08:00
createTestTransaction(dispatch, fetchAccount, url, clusterStatus);
2020-03-26 22:35:02 +08:00
}
2020-05-07 16:41:42 +08:00
}, [testFlag, cluster, clusterStatus, url]); // eslint-disable-line react-hooks/exhaustive-deps
2020-04-29 20:48:38 +08:00
// Check for transactions in the url params
2020-05-02 19:34:44 +08:00
const values = TX_ALIASES.flatMap(key => [
query.get(key),
query.get(key + "s")
]);
2020-04-29 20:48:38 +08:00
React.useEffect(() => {
2020-05-02 19:34:44 +08:00
values
2020-05-02 12:55:12 +08:00
.filter((value): value is string => value !== null)
.flatMap(value => value.split(","))
// Remove duplicates
.filter((item, pos, self) => self.indexOf(item) === pos)
2020-04-29 20:48:38 +08:00
.filter(signature => !state.transactions[signature])
.forEach(signature => {
2020-05-07 16:41:42 +08:00
fetchTransactionStatus(dispatch, signature, url, clusterStatus);
2020-04-29 20:48:38 +08:00
});
2020-05-02 19:34:44 +08:00
}, [values.toString()]); // eslint-disable-line react-hooks/exhaustive-deps
2020-04-29 20:48:38 +08:00
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
<DetailsProvider>{children}</DetailsProvider>
</DispatchContext.Provider>
</StateContext.Provider>
);
}
async function createTestTransaction(
dispatch: Dispatch,
2020-05-12 18:32:14 +08:00
fetchAccount: (pubkey: PublicKey) => void,
2020-05-07 16:41:42 +08:00
url: string,
clusterStatus: ClusterStatus
) {
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"
);
2020-05-07 16:41:42 +08:00
fetchTransactionStatus(dispatch, signature, url, clusterStatus);
2020-05-12 18:32:14 +08:00
fetchAccount(testAccount.publicKey);
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 sendAndConfirmTransaction(
connection,
tx,
[testAccount],
1
);
2020-05-07 16:41:42 +08:00
fetchTransactionStatus(dispatch, signature, url, clusterStatus);
} catch (error) {
console.error("Failed to create test failure transaction", error);
2020-03-26 22:35:02 +08:00
}
}
2020-05-07 16:41:42 +08:00
export async function fetchTransactionStatus(
dispatch: Dispatch,
2020-03-19 22:31:05 +08:00
signature: TransactionSignature,
2020-05-07 16:41:42 +08:00
url: string,
status: ClusterStatus
) {
dispatch({
2020-05-12 18:32:14 +08:00
type: ActionType.FetchSignature,
signature
});
2020-05-07 16:41:42 +08:00
// We will auto-refetch when status is no longer connecting
if (status === ClusterStatus.Connecting) return;
2020-04-29 11:00:06 +08:00
let fetchStatus;
2020-04-29 20:48:38 +08:00
let info: TransactionStatusInfo | undefined;
try {
const connection = new Connection(url);
const { value } = await connection.getSignatureStatus(signature, {
2020-04-07 00:30:34 +08:00
searchTransactionHistory: true
});
2020-04-29 11:00:06 +08:00
if (value !== null) {
2020-05-23 16:11:16 +08:00
let blockTime = await connection.getBlockTime(value.slot);
let timestamp: Timestamp = blockTime !== null ? blockTime : "unavailable";
2020-05-23 16:11:16 +08:00
2020-04-29 11:00:06 +08:00
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 20:48:38 +08:00
info = {
2020-04-29 11:00:06 +08:00
slot: value.slot,
timestamp,
2020-04-29 11:00:06 +08:00
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;
}
2020-04-29 20:48:38 +08:00
dispatch({
type: ActionType.UpdateStatus,
2020-04-29 11:00:06 +08:00
signature,
fetchStatus,
2020-04-29 20:48:38 +08:00
info
});
}
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,
transactions: Object.values(context.transactions).sort((a, b) =>
a.id <= b.id ? 1 : -1
)
};
}
2020-04-29 20:48:38 +08:00
export function useTransactionStatus(signature: TransactionSignature) {
const context = React.useContext(StateContext);
if (!context) {
throw new Error(
`useTransactionStatus must be used within a TransactionsProvider`
);
}
return context.transactions[signature];
}
export function useTransactionDetails(signature: TransactionSignature) {
const context = React.useContext(DetailsStateContext);
if (!context) {
throw new Error(
`useTransactionDetails must be used within a TransactionsProvider`
);
}
return context[signature];
}
2020-05-07 16:41:42 +08:00
export function useDetailsDispatch() {
const context = React.useContext(DetailsDispatchContext);
if (!context) {
throw new Error(
2020-05-07 16:41:42 +08:00
`useDetailsDispatch must be used within a TransactionsProvider`
);
}
return context;
}
2020-05-07 16:41:42 +08:00
export function useFetchTransactionStatus() {
const dispatch = React.useContext(DispatchContext);
if (!dispatch) {
throw new Error(
2020-05-07 16:41:42 +08:00
`useFetchTransactionStatus must be used within a TransactionsProvider`
);
}
2020-05-07 16:41:42 +08:00
const { url, status } = useCluster();
return (signature: TransactionSignature) => {
2020-05-07 16:41:42 +08:00
fetchTransactionStatus(dispatch, signature, url, status);
};
}