2020-03-17 12:04:04 +08:00
|
|
|
import React from "react";
|
2020-04-04 17:24:25 +08:00
|
|
|
import {
|
|
|
|
TransactionSignature,
|
|
|
|
Connection,
|
|
|
|
SystemProgram,
|
2020-04-29 11:00:06 +08:00
|
|
|
Account,
|
|
|
|
SignatureResult
|
2020-04-04 17:24:25 +08:00
|
|
|
} from "@solana/web3.js";
|
2020-04-25 00:31:32 +08:00
|
|
|
import { findGetParameter, findPathSegment } from "../../utils/url";
|
|
|
|
import { useCluster, ClusterStatus } from "../cluster";
|
|
|
|
import {
|
|
|
|
DetailsProvider,
|
|
|
|
StateContext as DetailsStateContext,
|
|
|
|
DispatchContext as DetailsDispatchContext
|
|
|
|
} from "./details";
|
2020-04-04 17:24:25 +08:00
|
|
|
import base58 from "bs58";
|
2020-04-09 16:09:53 +08:00
|
|
|
import {
|
|
|
|
useAccountsDispatch,
|
|
|
|
fetchAccountInfo,
|
|
|
|
Dispatch as AccountsDispatch,
|
|
|
|
ActionType as AccountsActionType
|
2020-04-25 00:31:32 +08:00
|
|
|
} from "../accounts";
|
2020-03-17 12:04:04 +08:00
|
|
|
|
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-17 12:04:04 +08:00
|
|
|
}
|
|
|
|
|
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-03-17 12:04:04 +08:00
|
|
|
}
|
|
|
|
|
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 };
|
2020-03-17 12:04:04 +08:00
|
|
|
interface State {
|
|
|
|
idCounter: number;
|
2020-04-29 11:00:06 +08:00
|
|
|
selected?: TransactionSignature;
|
2020-03-17 12:04:04 +08:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2020-03-17 12:04:04 +08:00
|
|
|
interface UpdateStatus {
|
2020-03-19 22:31:05 +08:00
|
|
|
type: ActionType.UpdateStatus;
|
2020-04-01 16:35:12 +08:00
|
|
|
signature: TransactionSignature;
|
2020-04-29 11:00:06 +08:00
|
|
|
fetchStatus: FetchStatus;
|
|
|
|
transactionStatus?: TransactionStatus;
|
2020-03-17 12:04:04 +08:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
2020-03-17 12:04:04 +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: {
|
2020-04-01 16:35:12 +08:00
|
|
|
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,
|
2020-04-01 16:35:12 +08:00
|
|
|
[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: {
|
2020-04-01 16:35:12 +08:00
|
|
|
let transaction = state.transactions[action.signature];
|
2020-03-19 22:31:05 +08:00
|
|
|
if (transaction) {
|
2020-03-25 12:21:47 +08:00
|
|
|
transaction = {
|
|
|
|
...transaction,
|
2020-04-29 11:00:06 +08:00
|
|
|
fetchStatus: action.fetchStatus
|
2020-03-25 12:21:47 +08:00
|
|
|
};
|
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,
|
2020-04-01 16:35:12 +08:00
|
|
|
[action.signature]: transaction
|
2020-03-19 22:31:05 +08:00
|
|
|
};
|
|
|
|
return { ...state, transactions };
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2020-03-17 12:04:04 +08:00
|
|
|
}
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
2020-04-06 01:34:04 +08:00
|
|
|
export const TX_PATHS = [
|
2020-04-24 23:12:37 +08:00
|
|
|
"/tx",
|
|
|
|
"/txs",
|
|
|
|
"/txn",
|
|
|
|
"/txns",
|
|
|
|
"/transaction",
|
|
|
|
"/transactions"
|
2020-04-06 01:34:04 +08:00
|
|
|
];
|
|
|
|
|
2020-03-30 23:34:03 +08:00
|
|
|
function urlSignatures(): Array<string> {
|
|
|
|
const signatures: Array<string> = [];
|
2020-04-06 01:34:04 +08:00
|
|
|
|
|
|
|
TX_PATHS.forEach(path => {
|
2020-04-24 23:12:37 +08:00
|
|
|
const name = path.slice(1);
|
|
|
|
const params = findGetParameter(name)?.split(",") || [];
|
|
|
|
const segments = findPathSegment(name)?.split(",") || [];
|
2020-04-06 01:34:04 +08:00
|
|
|
signatures.push(...params);
|
|
|
|
signatures.push(...segments);
|
|
|
|
});
|
|
|
|
|
|
|
|
return signatures.filter(s => s.length > 0);
|
2020-03-30 23:34:03 +08:00
|
|
|
}
|
|
|
|
|
2020-03-17 12:04:04 +08:00
|
|
|
function initState(): State {
|
|
|
|
let idCounter = 0;
|
2020-03-30 23:34:03 +08:00
|
|
|
const signatures = urlSignatures();
|
2020-03-17 12:04:04 +08:00
|
|
|
const transactions = signatures.reduce(
|
|
|
|
(transactions: Transactions, signature) => {
|
2020-04-01 16:35:12 +08:00
|
|
|
if (!!transactions[signature]) return transactions;
|
2020-04-29 11:00:06 +08:00
|
|
|
const nextId = idCounter + 1;
|
2020-04-01 16:35:12 +08:00
|
|
|
transactions[signature] = {
|
2020-04-29 11:00:06 +08:00
|
|
|
id: nextId,
|
|
|
|
source: Source.Url,
|
2020-04-01 16:35:12 +08:00
|
|
|
signature,
|
2020-04-29 11:00:06 +08:00
|
|
|
fetchStatus: FetchStatus.Fetching
|
2020-03-17 12:04:04 +08:00
|
|
|
};
|
2020-04-29 11:00:06 +08:00
|
|
|
idCounter++;
|
2020-03-17 12:04:04 +08:00
|
|
|
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-04-09 16:09:53 +08:00
|
|
|
const accountsDispatch = useAccountsDispatch();
|
2020-03-17 12:04:04 +08:00
|
|
|
|
2020-03-31 14:36:40 +08:00
|
|
|
// Check transaction statuses on startup and whenever cluster updates
|
2020-03-17 12:04:04 +08:00
|
|
|
React.useEffect(() => {
|
2020-03-26 22:35:02 +08:00
|
|
|
if (status !== ClusterStatus.Connected) return;
|
|
|
|
|
|
|
|
// Create a test transaction
|
2020-04-04 17:24:25 +08:00
|
|
|
if (findGetParameter("test") !== null) {
|
2020-04-09 16:09:53 +08:00
|
|
|
createTestTransaction(dispatch, accountsDispatch, url);
|
2020-03-26 22:35:02 +08:00
|
|
|
}
|
|
|
|
|
2020-04-01 16:35:12 +08:00
|
|
|
Object.keys(state.transactions).forEach(signature => {
|
|
|
|
checkTransactionStatus(dispatch, signature, url);
|
2020-03-17 12:04:04 +08:00
|
|
|
});
|
|
|
|
}, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
|
|
|
return (
|
|
|
|
<StateContext.Provider value={state}>
|
|
|
|
<DispatchContext.Provider value={dispatch}>
|
2020-04-25 00:31:32 +08:00
|
|
|
<DetailsProvider>{children}</DetailsProvider>
|
2020-03-17 12:04:04 +08:00
|
|
|
</DispatchContext.Provider>
|
|
|
|
</StateContext.Provider>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-09 16:09:53 +08:00
|
|
|
async function createTestTransaction(
|
|
|
|
dispatch: Dispatch,
|
|
|
|
accountsDispatch: AccountsDispatch,
|
|
|
|
url: string
|
|
|
|
) {
|
2020-04-04 17:24:25 +08:00
|
|
|
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 {
|
2020-04-01 23:38:36 +08:00
|
|
|
const connection = new Connection(url, "recent");
|
2020-03-26 22:35:02 +08:00
|
|
|
const signature = await connection.requestAirdrop(
|
2020-04-04 17:24:25 +08:00
|
|
|
testAccount.publicKey,
|
|
|
|
100000,
|
2020-03-26 22:35:02 +08:00
|
|
|
"recent"
|
|
|
|
);
|
|
|
|
dispatch({ type: ActionType.InputSignature, signature });
|
2020-04-01 16:35:12 +08:00
|
|
|
checkTransactionStatus(dispatch, signature, url);
|
2020-04-09 16:09:53 +08:00
|
|
|
accountsDispatch({
|
|
|
|
type: AccountsActionType.Input,
|
|
|
|
pubkey: testAccount.publicKey
|
|
|
|
});
|
|
|
|
fetchAccountInfo(accountsDispatch, testAccount.publicKey.toBase58(), url);
|
2020-03-26 22:35:02 +08:00
|
|
|
} catch (error) {
|
2020-04-04 17:24:25 +08:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-17 12:04:04 +08:00
|
|
|
export async function checkTransactionStatus(
|
|
|
|
dispatch: Dispatch,
|
2020-03-19 22:31:05 +08:00
|
|
|
signature: TransactionSignature,
|
|
|
|
url: string
|
2020-03-17 12:04:04 +08:00
|
|
|
) {
|
|
|
|
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-03-17 12:04:04 +08:00
|
|
|
});
|
|
|
|
|
2020-04-29 11:00:06 +08:00
|
|
|
let fetchStatus;
|
|
|
|
let transactionStatus: TransactionStatus | undefined;
|
2020-03-17 12:04:04 +08:00
|
|
|
try {
|
2020-04-07 00:30:34 +08:00
|
|
|
const { value } = await new Connection(url).getSignatureStatus(signature, {
|
|
|
|
searchTransactionHistory: true
|
|
|
|
});
|
2020-03-17 12:04:04 +08:00
|
|
|
|
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-03-17 12:04:04 +08:00
|
|
|
}
|
2020-04-29 11:00:06 +08:00
|
|
|
fetchStatus = FetchStatus.Fetched;
|
2020-03-17 12:04:04 +08:00
|
|
|
} catch (error) {
|
2020-04-29 11:00:06 +08:00
|
|
|
console.error("Failed to fetch transaction status", error);
|
|
|
|
fetchStatus = FetchStatus.FetchFailed;
|
2020-03-17 12:04:04 +08:00
|
|
|
}
|
2020-04-01 16:35:12 +08:00
|
|
|
dispatch({
|
|
|
|
type: ActionType.UpdateStatus,
|
2020-04-29 11:00:06 +08:00
|
|
|
signature,
|
|
|
|
fetchStatus,
|
|
|
|
transactionStatus
|
2020-04-01 16:35:12 +08:00
|
|
|
});
|
2020-03-17 12:04:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
)
|
|
|
|
};
|
2020-03-17 12:04:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
export function useTransactionsDispatch() {
|
|
|
|
const context = React.useContext(DispatchContext);
|
|
|
|
if (!context) {
|
|
|
|
throw new Error(
|
|
|
|
`useTransactionsDispatch must be used within a TransactionsProvider`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return context;
|
|
|
|
}
|
2020-04-25 00:31:32 +08:00
|
|
|
|
|
|
|
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];
|
|
|
|
}
|