Call getConfirmedTransaction instead of getConfirmedBlock for tx details
This commit is contained in:
parent
60b0eb2e57
commit
f3e677eaab
|
@ -1,12 +1,12 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
|
useDetails,
|
||||||
useTransactions,
|
useTransactions,
|
||||||
useTransactionsDispatch,
|
useTransactionsDispatch,
|
||||||
ActionType,
|
ActionType,
|
||||||
Selected
|
Selected
|
||||||
} from "../providers/transactions";
|
} from "../providers/transactions";
|
||||||
import { displayAddress, decodeCreate, decodeTransfer } from "../utils/tx";
|
import { displayAddress, decodeCreate, decodeTransfer } from "../utils/tx";
|
||||||
import { useBlocks } from "../providers/blocks";
|
|
||||||
import {
|
import {
|
||||||
LAMPORTS_PER_SOL,
|
LAMPORTS_PER_SOL,
|
||||||
TransferParams,
|
TransferParams,
|
||||||
|
@ -54,8 +54,7 @@ function TransactionModal() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function TransactionDetails({ selected }: { selected: Selected }) {
|
function TransactionDetails({ selected }: { selected: Selected }) {
|
||||||
const { blocks } = useBlocks();
|
const details = useDetails(selected.signature);
|
||||||
const block = blocks[selected.slot];
|
|
||||||
|
|
||||||
const renderError = (content: React.ReactNode) => {
|
const renderError = (content: React.ReactNode) => {
|
||||||
return (
|
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(
|
return renderError(
|
||||||
<>
|
<>
|
||||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
<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) return renderError("Transaction not found");
|
||||||
|
|
||||||
if (transaction.instructions.length === 0)
|
if (transaction.instructions.length === 0)
|
||||||
|
|
|
@ -5,7 +5,6 @@ import "./scss/theme.scss";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import * as serviceWorker from "./serviceWorker";
|
import * as serviceWorker from "./serviceWorker";
|
||||||
import { ClusterProvider } from "./providers/cluster";
|
import { ClusterProvider } from "./providers/cluster";
|
||||||
import { BlocksProvider } from "./providers/blocks";
|
|
||||||
import { TransactionsProvider } from "./providers/transactions";
|
import { TransactionsProvider } from "./providers/transactions";
|
||||||
import { AccountsProvider } from "./providers/accounts";
|
import { AccountsProvider } from "./providers/accounts";
|
||||||
import { TabProvider } from "./providers/tab";
|
import { TabProvider } from "./providers/tab";
|
||||||
|
@ -16,9 +15,7 @@ ReactDOM.render(
|
||||||
<ClusterProvider>
|
<ClusterProvider>
|
||||||
<AccountsProvider>
|
<AccountsProvider>
|
||||||
<TransactionsProvider>
|
<TransactionsProvider>
|
||||||
<BlocksProvider>
|
<App />
|
||||||
<App />
|
|
||||||
</BlocksProvider>
|
|
||||||
</TransactionsProvider>
|
</TransactionsProvider>
|
||||||
</AccountsProvider>
|
</AccountsProvider>
|
||||||
</ClusterProvider>
|
</ClusterProvider>
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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 });
|
||||||
|
}
|
|
@ -5,15 +5,20 @@ import {
|
||||||
SystemProgram,
|
SystemProgram,
|
||||||
Account
|
Account
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { findGetParameter, findPathSegment } from "../utils/url";
|
import { findGetParameter, findPathSegment } from "../../utils/url";
|
||||||
import { useCluster, ClusterStatus } from "../providers/cluster";
|
import { useCluster, ClusterStatus } from "../cluster";
|
||||||
|
import {
|
||||||
|
DetailsProvider,
|
||||||
|
StateContext as DetailsStateContext,
|
||||||
|
DispatchContext as DetailsDispatchContext
|
||||||
|
} from "./details";
|
||||||
import base58 from "bs58";
|
import base58 from "bs58";
|
||||||
import {
|
import {
|
||||||
useAccountsDispatch,
|
useAccountsDispatch,
|
||||||
fetchAccountInfo,
|
fetchAccountInfo,
|
||||||
Dispatch as AccountsDispatch,
|
Dispatch as AccountsDispatch,
|
||||||
ActionType as AccountsActionType
|
ActionType as AccountsActionType
|
||||||
} from "./accounts";
|
} from "../accounts";
|
||||||
|
|
||||||
export enum Status {
|
export enum Status {
|
||||||
Checking,
|
Checking,
|
||||||
|
@ -207,7 +212,7 @@ export function TransactionsProvider({ children }: TransactionsProviderProps) {
|
||||||
return (
|
return (
|
||||||
<StateContext.Provider value={state}>
|
<StateContext.Provider value={state}>
|
||||||
<DispatchContext.Provider value={dispatch}>
|
<DispatchContext.Provider value={dispatch}>
|
||||||
{children}
|
<DetailsProvider>{children}</DetailsProvider>
|
||||||
</DispatchContext.Provider>
|
</DispatchContext.Provider>
|
||||||
</StateContext.Provider>
|
</StateContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -330,3 +335,21 @@ export function useTransactionsDispatch() {
|
||||||
}
|
}
|
||||||
return context;
|
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];
|
||||||
|
}
|
|
@ -13,7 +13,8 @@
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react"
|
"jsx": "react",
|
||||||
|
"baseUrl": "src"
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue