Call getConfirmedTransaction instead of getConfirmedBlock for tx details

This commit is contained in:
Justin Starry 2020-04-25 00:31:32 +08:00 committed by Michael Vines
parent 60b0eb2e57
commit f3e677eaab
6 changed files with 184 additions and 191 deletions

View File

@ -1,12 +1,12 @@
import React from "react";
import {
useDetails,
useTransactions,
useTransactionsDispatch,
ActionType,
Selected
} from "../providers/transactions";
import { displayAddress, decodeCreate, decodeTransfer } from "../utils/tx";
import { useBlocks } from "../providers/blocks";
import {
LAMPORTS_PER_SOL,
TransferParams,
@ -54,8 +54,7 @@ function TransactionModal() {
}
function TransactionDetails({ selected }: { selected: Selected }) {
const { blocks } = useBlocks();
const block = blocks[selected.slot];
const details = useDetails(selected.signature);
const renderError = (content: React.ReactNode) => {
return (
@ -65,9 +64,9 @@ function TransactionDetails({ selected }: { selected: Selected }) {
);
};
if (!block) return renderError("Transaction block not found");
if (!details) return renderError("Transaction details not found");
if (!block.transactions) {
if (!details.transaction) {
return renderError(
<>
<span className="spinner-grow spinner-grow-sm mr-2"></span>
@ -76,7 +75,7 @@ function TransactionDetails({ selected }: { selected: Selected }) {
);
}
const transaction = block.transactions[selected.signature];
const { transaction } = details.transaction;
if (!transaction) return renderError("Transaction not found");
if (transaction.instructions.length === 0)

View File

@ -5,7 +5,6 @@ import "./scss/theme.scss";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { ClusterProvider } from "./providers/cluster";
import { BlocksProvider } from "./providers/blocks";
import { TransactionsProvider } from "./providers/transactions";
import { AccountsProvider } from "./providers/accounts";
import { TabProvider } from "./providers/tab";
@ -16,9 +15,7 @@ ReactDOM.render(
<ClusterProvider>
<AccountsProvider>
<TransactionsProvider>
<BlocksProvider>
<App />
</BlocksProvider>
<App />
</TransactionsProvider>
</AccountsProvider>
</ClusterProvider>

View File

@ -1,176 +0,0 @@
import React from "react";
import bs58 from "bs58";
import { Connection, Transaction } from "@solana/web3.js";
import { useCluster, ClusterStatus } from "./cluster";
import { useTransactions } from "./transactions";
export enum Status {
Checking,
CheckFailed,
Success
}
type Transactions = { [signature: string]: Transaction };
export interface Block {
status: Status;
transactions?: Transactions;
}
export type Blocks = { [slot: number]: Block };
interface State {
blocks: Blocks;
}
export enum ActionType {
Update,
Add,
Remove
}
interface Update {
type: ActionType.Update;
slot: number;
status: Status;
transactions?: Transactions;
}
interface Add {
type: ActionType.Add;
slots: number[];
}
interface Remove {
type: ActionType.Remove;
slots: number[];
}
type Action = Update | Add | Remove;
type Dispatch = (action: Action) => void;
function reducer(state: State, action: Action): State {
switch (action.type) {
case ActionType.Add: {
if (action.slots.length === 0) return state;
const blocks = { ...state.blocks };
action.slots.forEach(slot => {
if (!blocks[slot]) {
blocks[slot] = {
status: Status.Checking
};
}
});
return { ...state, blocks };
}
case ActionType.Remove: {
if (action.slots.length === 0) return state;
const blocks = { ...state.blocks };
action.slots.forEach(slot => {
delete blocks[slot];
});
return { ...state, blocks };
}
case ActionType.Update: {
let block = state.blocks[action.slot];
if (block) {
block = {
...block,
status: action.status,
transactions: action.transactions
};
const blocks = {
...state.blocks,
[action.slot]: block
};
return { ...state, blocks };
}
break;
}
}
return state;
}
const StateContext = React.createContext<State | undefined>(undefined);
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
type BlocksProviderProps = { children: React.ReactNode };
export function BlocksProvider({ children }: BlocksProviderProps) {
const [state, dispatch] = React.useReducer(reducer, { blocks: {} });
const { transactions } = useTransactions();
const { status, url } = useCluster();
// Filter blocks for current transaction slots
React.useEffect(() => {
if (status !== ClusterStatus.Connected) return;
const remove: number[] = [];
const txSlots = transactions
.map(tx => tx.slot)
.filter(x => x)
.reduce((set, slot) => set.add(slot), new Set());
Object.keys(state.blocks).forEach(blockKey => {
const slot = parseInt(blockKey);
if (!txSlots.has(slot)) {
remove.push(slot);
}
});
dispatch({ type: ActionType.Remove, slots: remove });
const fetchSlots = new Set<number>();
transactions.forEach(tx => {
if (tx.slot && tx.confirmations === "max" && !state.blocks[tx.slot])
fetchSlots.add(tx.slot);
});
const fetchList: number[] = [];
fetchSlots.forEach(s => fetchList.push(s));
dispatch({ type: ActionType.Add, slots: fetchList });
fetchSlots.forEach(slot => {
fetchBlock(dispatch, slot, url);
});
}, [transactions]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
async function fetchBlock(dispatch: Dispatch, slot: number, url: string) {
dispatch({
type: ActionType.Update,
status: Status.Checking,
slot
});
let status;
let transactions: Transactions = {};
try {
const block = await new Connection(url).getConfirmedBlock(slot);
block.transactions.forEach(({ transaction }) => {
const signature = transaction.signature;
if (signature) {
const sig = bs58.encode(signature);
transactions[sig] = transaction;
}
});
status = Status.Success;
} catch (error) {
console.error("Failed to fetch confirmed block", error);
status = Status.CheckFailed;
}
dispatch({ type: ActionType.Update, status, slot, transactions });
}
export function useBlocks() {
const context = React.useContext(StateContext);
if (!context) {
throw new Error(`useBlocks must be used within a BlocksProvider`);
}
return context;
}

View File

@ -0,0 +1,149 @@
import React from "react";
import {
Connection,
TransactionSignature,
ConfirmedTransaction
} from "@solana/web3.js";
import { useCluster, ClusterStatus } from "../cluster";
import { useTransactions } from "./index";
export enum Status {
Checking,
CheckFailed,
NotFound,
Found
}
export interface Details {
status: Status;
transaction: ConfirmedTransaction | null;
}
type State = { [signature: string]: Details };
export enum ActionType {
Update,
Add
}
interface Update {
type: ActionType.Update;
signature: string;
status: Status;
transaction: ConfirmedTransaction | null;
}
interface Add {
type: ActionType.Add;
signatures: TransactionSignature[];
}
type Action = Update | Add;
type Dispatch = (action: Action) => void;
function reducer(state: State, action: Action): State {
switch (action.type) {
case ActionType.Add: {
if (action.signatures.length === 0) return state;
const details = { ...state };
action.signatures.forEach(signature => {
if (!details[signature]) {
details[signature] = {
status: Status.Checking,
transaction: null
};
}
});
return details;
}
case ActionType.Update: {
let details = state[action.signature];
if (details) {
details = {
...details,
status: action.status
};
if (action.transaction !== null) {
details.transaction = action.transaction;
}
return {
...state,
...{
[action.signature]: details
}
};
}
break;
}
}
return state;
}
export const StateContext = React.createContext<State | undefined>(undefined);
export const DispatchContext = React.createContext<Dispatch | undefined>(
undefined
);
type DetailsProviderProps = { children: React.ReactNode };
export function DetailsProvider({ children }: DetailsProviderProps) {
const [state, dispatch] = React.useReducer(reducer, {});
const { transactions } = useTransactions();
const { status, url } = useCluster();
// Filter blocks for current transaction slots
React.useEffect(() => {
if (status !== ClusterStatus.Connected) return;
const fetchSignatures = new Set<string>();
transactions.forEach(tx => {
if (tx.slot && tx.confirmations === "max" && !state[tx.signature])
fetchSignatures.add(tx.signature);
});
const fetchList: string[] = [];
fetchSignatures.forEach(s => fetchList.push(s));
dispatch({ type: ActionType.Add, signatures: fetchList });
fetchSignatures.forEach(signature => {
fetchDetails(dispatch, signature, url);
});
}, [transactions]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
async function fetchDetails(
dispatch: Dispatch,
signature: TransactionSignature,
url: string
) {
dispatch({
type: ActionType.Update,
status: Status.Checking,
transaction: null,
signature
});
let status;
let transaction = null;
try {
transaction = await new Connection(url).getConfirmedTransaction(signature);
if (transaction) {
status = Status.Found;
} else {
status = Status.NotFound;
}
} catch (error) {
console.error("Failed to fetch confirmed transaction", error);
status = Status.CheckFailed;
}
dispatch({ type: ActionType.Update, status, signature, transaction });
}

View File

@ -5,15 +5,20 @@ import {
SystemProgram,
Account
} from "@solana/web3.js";
import { findGetParameter, findPathSegment } from "../utils/url";
import { useCluster, ClusterStatus } from "../providers/cluster";
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";
} from "../accounts";
export enum Status {
Checking,
@ -207,7 +212,7 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
<DetailsProvider>{children}</DetailsProvider>
</DispatchContext.Provider>
</StateContext.Provider>
);
@ -330,3 +335,21 @@ export function useTransactionsDispatch() {
}
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];
}

View File

@ -13,7 +13,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
"jsx": "react",
"baseUrl": "src"
},
"include": ["src"]
}