2020-03-31 06:58:48 -07:00
|
|
|
import React from "react";
|
2020-04-21 08:30:52 -07:00
|
|
|
import {
|
|
|
|
PublicKey,
|
|
|
|
Connection,
|
|
|
|
TransactionSignature,
|
|
|
|
TransactionError,
|
|
|
|
SignatureStatus
|
|
|
|
} from "@solana/web3.js";
|
2020-04-09 02:49:47 -07:00
|
|
|
import { findGetParameter, findPathSegment } from "../utils/url";
|
2020-03-26 07:35:02 -07:00
|
|
|
import { useCluster, ClusterStatus } from "./cluster";
|
2020-03-31 06:58:48 -07:00
|
|
|
|
|
|
|
export enum Status {
|
|
|
|
Checking,
|
|
|
|
CheckFailed,
|
2020-04-21 08:30:52 -07:00
|
|
|
FetchingHistory,
|
|
|
|
HistoryFailed,
|
2020-04-05 01:31:40 -07:00
|
|
|
NotFound,
|
2020-03-31 06:58:48 -07:00
|
|
|
Success
|
|
|
|
}
|
|
|
|
|
2020-04-21 08:30:52 -07:00
|
|
|
export type History = Map<
|
|
|
|
number,
|
|
|
|
Map<TransactionSignature, TransactionError | null>
|
|
|
|
>;
|
|
|
|
|
2020-03-31 06:58:48 -07:00
|
|
|
export interface Details {
|
|
|
|
executable: boolean;
|
|
|
|
owner: PublicKey;
|
|
|
|
space: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface Account {
|
|
|
|
id: number;
|
|
|
|
pubkey: PublicKey;
|
2020-04-21 08:30:52 -07:00
|
|
|
status: Status;
|
2020-04-05 01:31:40 -07:00
|
|
|
lamports?: number;
|
2020-03-31 06:58:48 -07:00
|
|
|
details?: Details;
|
2020-04-21 08:30:52 -07:00
|
|
|
history?: History;
|
2020-03-31 06:58:48 -07:00
|
|
|
}
|
|
|
|
|
2020-04-01 01:35:12 -07:00
|
|
|
type Accounts = { [address: string]: Account };
|
2020-03-31 06:58:48 -07:00
|
|
|
interface State {
|
|
|
|
idCounter: number;
|
|
|
|
accounts: Accounts;
|
2020-04-21 08:30:52 -07:00
|
|
|
selected?: string;
|
2020-03-31 06:58:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export enum ActionType {
|
|
|
|
Update,
|
2020-04-21 08:30:52 -07:00
|
|
|
Input,
|
|
|
|
Select
|
2020-03-31 06:58:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
interface Update {
|
|
|
|
type: ActionType.Update;
|
2020-04-01 01:35:12 -07:00
|
|
|
address: string;
|
2020-04-21 08:30:52 -07:00
|
|
|
data: {
|
|
|
|
status: Status;
|
|
|
|
lamports?: number;
|
|
|
|
details?: Details;
|
|
|
|
history?: History;
|
|
|
|
};
|
2020-03-31 06:58:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
interface Input {
|
|
|
|
type: ActionType.Input;
|
|
|
|
pubkey: PublicKey;
|
|
|
|
}
|
|
|
|
|
2020-04-21 08:30:52 -07:00
|
|
|
interface Select {
|
|
|
|
type: ActionType.Select;
|
|
|
|
address?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
type Action = Update | Input | Select;
|
2020-04-09 01:09:53 -07:00
|
|
|
export type Dispatch = (action: Action) => void;
|
2020-03-31 06:58:48 -07:00
|
|
|
|
|
|
|
function reducer(state: State, action: Action): State {
|
|
|
|
switch (action.type) {
|
|
|
|
case ActionType.Input: {
|
2020-04-01 01:35:12 -07:00
|
|
|
const address = action.pubkey.toBase58();
|
|
|
|
if (!!state.accounts[address]) return state;
|
2020-03-31 06:58:48 -07:00
|
|
|
const idCounter = state.idCounter + 1;
|
|
|
|
const accounts = {
|
|
|
|
...state.accounts,
|
2020-04-01 01:35:12 -07:00
|
|
|
[address]: {
|
2020-03-31 06:58:48 -07:00
|
|
|
id: idCounter,
|
|
|
|
status: Status.Checking,
|
|
|
|
pubkey: action.pubkey
|
|
|
|
}
|
|
|
|
};
|
|
|
|
return { ...state, accounts, idCounter };
|
|
|
|
}
|
2020-04-21 08:30:52 -07:00
|
|
|
|
|
|
|
case ActionType.Select: {
|
|
|
|
return { ...state, selected: action.address };
|
|
|
|
}
|
|
|
|
|
2020-03-31 06:58:48 -07:00
|
|
|
case ActionType.Update: {
|
2020-04-01 01:35:12 -07:00
|
|
|
let account = state.accounts[action.address];
|
2020-03-31 06:58:48 -07:00
|
|
|
if (account) {
|
|
|
|
account = {
|
|
|
|
...account,
|
2020-04-21 08:30:52 -07:00
|
|
|
...action.data
|
2020-03-31 06:58:48 -07:00
|
|
|
};
|
|
|
|
const accounts = {
|
|
|
|
...state.accounts,
|
2020-04-01 01:35:12 -07:00
|
|
|
[action.address]: account
|
2020-03-31 06:58:48 -07:00
|
|
|
};
|
|
|
|
return { ...state, accounts };
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
2020-04-24 08:12:37 -07:00
|
|
|
export const ACCOUNT_PATHS = [
|
|
|
|
"/account",
|
|
|
|
"/accounts",
|
|
|
|
"/address",
|
|
|
|
"/addresses"
|
|
|
|
];
|
2020-04-05 10:34:04 -07:00
|
|
|
|
2020-04-01 01:35:12 -07:00
|
|
|
function urlAddresses(): Array<string> {
|
|
|
|
const addresses: Array<string> = [];
|
2020-04-05 10:34:04 -07:00
|
|
|
|
|
|
|
ACCOUNT_PATHS.forEach(path => {
|
2020-04-24 08:12:37 -07:00
|
|
|
const name = path.slice(1);
|
|
|
|
const params = findGetParameter(name)?.split(",") || [];
|
|
|
|
const segments = findPathSegment(name)?.split(",") || [];
|
2020-04-05 10:34:04 -07:00
|
|
|
addresses.push(...params);
|
|
|
|
addresses.push(...segments);
|
|
|
|
});
|
|
|
|
|
|
|
|
return addresses.filter(a => a.length > 0);
|
2020-03-31 06:58:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
function initState(): State {
|
|
|
|
let idCounter = 0;
|
2020-04-01 01:35:12 -07:00
|
|
|
const addresses = urlAddresses();
|
|
|
|
const accounts = addresses.reduce((accounts: Accounts, address) => {
|
|
|
|
if (!!accounts[address]) return accounts;
|
|
|
|
try {
|
|
|
|
const pubkey = new PublicKey(address);
|
|
|
|
const id = ++idCounter;
|
|
|
|
accounts[address] = {
|
|
|
|
id,
|
|
|
|
status: Status.Checking,
|
|
|
|
pubkey
|
|
|
|
};
|
|
|
|
} catch (err) {
|
|
|
|
// TODO display to user
|
|
|
|
console.error(err);
|
|
|
|
}
|
2020-03-31 06:58:48 -07:00
|
|
|
return accounts;
|
|
|
|
}, {});
|
|
|
|
return { idCounter, accounts };
|
|
|
|
}
|
|
|
|
|
|
|
|
const StateContext = React.createContext<State | undefined>(undefined);
|
|
|
|
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
|
|
|
|
|
|
|
type AccountsProviderProps = { children: React.ReactNode };
|
|
|
|
export function AccountsProvider({ children }: AccountsProviderProps) {
|
|
|
|
const [state, dispatch] = React.useReducer(reducer, undefined, initState);
|
|
|
|
|
|
|
|
const { status, url } = useCluster();
|
|
|
|
|
|
|
|
// Check account statuses on startup and whenever cluster updates
|
|
|
|
React.useEffect(() => {
|
2020-03-26 07:35:02 -07:00
|
|
|
if (status !== ClusterStatus.Connected) return;
|
|
|
|
|
2020-04-01 01:35:12 -07:00
|
|
|
Object.keys(state.accounts).forEach(address => {
|
|
|
|
fetchAccountInfo(dispatch, address, url);
|
2020-03-31 06:58:48 -07:00
|
|
|
});
|
|
|
|
}, [status, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
|
|
|
return (
|
|
|
|
<StateContext.Provider value={state}>
|
|
|
|
<DispatchContext.Provider value={dispatch}>
|
|
|
|
{children}
|
|
|
|
</DispatchContext.Provider>
|
|
|
|
</StateContext.Provider>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function fetchAccountInfo(
|
|
|
|
dispatch: Dispatch,
|
2020-04-01 01:35:12 -07:00
|
|
|
address: string,
|
2020-03-31 06:58:48 -07:00
|
|
|
url: string
|
|
|
|
) {
|
|
|
|
dispatch({
|
|
|
|
type: ActionType.Update,
|
2020-04-21 08:30:52 -07:00
|
|
|
address,
|
|
|
|
data: {
|
|
|
|
status: Status.Checking
|
|
|
|
}
|
2020-03-31 06:58:48 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
let status;
|
|
|
|
let details;
|
2020-04-05 01:31:40 -07:00
|
|
|
let lamports;
|
2020-03-31 06:58:48 -07:00
|
|
|
try {
|
2020-04-09 01:09:53 -07:00
|
|
|
const result = await new Connection(url, "recent").getAccountInfo(
|
2020-04-01 01:35:12 -07:00
|
|
|
new PublicKey(address)
|
|
|
|
);
|
2020-04-06 03:54:29 -07:00
|
|
|
if (result === null) {
|
2020-04-05 01:31:40 -07:00
|
|
|
lamports = 0;
|
|
|
|
status = Status.NotFound;
|
|
|
|
} else {
|
2020-04-06 03:54:29 -07:00
|
|
|
lamports = result.lamports;
|
|
|
|
details = {
|
|
|
|
space: result.data.length,
|
|
|
|
executable: result.executable,
|
|
|
|
owner: result.owner
|
|
|
|
};
|
2020-04-21 08:30:52 -07:00
|
|
|
status = Status.FetchingHistory;
|
|
|
|
fetchAccountHistory(dispatch, address, url);
|
2020-04-05 01:31:40 -07:00
|
|
|
}
|
2020-04-06 03:54:29 -07:00
|
|
|
} catch (error) {
|
|
|
|
console.error("Failed to fetch account info", error);
|
|
|
|
status = Status.CheckFailed;
|
2020-03-31 06:58:48 -07:00
|
|
|
}
|
2020-04-21 08:30:52 -07:00
|
|
|
const data = { status, lamports, details };
|
|
|
|
dispatch({ type: ActionType.Update, data, address });
|
|
|
|
}
|
|
|
|
|
|
|
|
async function fetchAccountHistory(
|
|
|
|
dispatch: Dispatch,
|
|
|
|
address: string,
|
|
|
|
url: string
|
|
|
|
) {
|
|
|
|
let history;
|
|
|
|
let status;
|
|
|
|
try {
|
|
|
|
const connection = new Connection(url);
|
|
|
|
const currentSlot = await connection.getSlot();
|
|
|
|
const signatures = await connection.getConfirmedSignaturesForAddress(
|
|
|
|
new PublicKey(address),
|
|
|
|
Math.max(0, currentSlot - 10000 + 1),
|
|
|
|
currentSlot
|
|
|
|
);
|
|
|
|
|
|
|
|
let statuses: (SignatureStatus | null)[] = [];
|
|
|
|
if (signatures.length > 0) {
|
|
|
|
statuses = (
|
|
|
|
await connection.getSignatureStatuses(signatures, {
|
|
|
|
searchTransactionHistory: true
|
|
|
|
})
|
|
|
|
).value;
|
|
|
|
}
|
|
|
|
|
|
|
|
history = new Map();
|
|
|
|
for (let i = 0; i < statuses.length; i++) {
|
|
|
|
const status = statuses[i];
|
|
|
|
if (!status) continue;
|
|
|
|
let slotSignatures = history.get(status.slot);
|
|
|
|
if (!slotSignatures) {
|
|
|
|
slotSignatures = new Map();
|
|
|
|
history.set(status.slot, slotSignatures);
|
|
|
|
}
|
|
|
|
slotSignatures.set(signatures[i], status.err);
|
|
|
|
}
|
|
|
|
status = Status.Success;
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Failed to fetch account history", error);
|
|
|
|
status = Status.HistoryFailed;
|
|
|
|
}
|
|
|
|
const data = { status, history };
|
|
|
|
dispatch({ type: ActionType.Update, data, address });
|
2020-03-31 06:58:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export function useAccounts() {
|
|
|
|
const context = React.useContext(StateContext);
|
|
|
|
if (!context) {
|
|
|
|
throw new Error(`useAccounts must be used within a AccountsProvider`);
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
idCounter: context.idCounter,
|
|
|
|
accounts: Object.values(context.accounts).sort((a, b) =>
|
|
|
|
a.id <= b.id ? 1 : -1
|
|
|
|
)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-04-21 08:30:52 -07:00
|
|
|
export function useSelectedAccount() {
|
|
|
|
const context = React.useContext(StateContext);
|
|
|
|
if (!context) {
|
|
|
|
throw new Error(
|
|
|
|
`useSelectedAccount must be used within a AccountsProvider`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!context.selected) return undefined;
|
|
|
|
return context.accounts[context.selected];
|
|
|
|
}
|
|
|
|
|
2020-03-31 06:58:48 -07:00
|
|
|
export function useAccountsDispatch() {
|
|
|
|
const context = React.useContext(DispatchContext);
|
|
|
|
if (!context) {
|
|
|
|
throw new Error(
|
|
|
|
`useAccountsDispatch must be used within a AccountsProvider`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return context;
|
|
|
|
}
|