2020-03-31 06:58:48 -07:00
|
|
|
import React from "react";
|
|
|
|
import { PublicKey, Connection } from "@solana/web3.js";
|
|
|
|
import { findGetParameter, findPathSegment } from "../utils";
|
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-05 01:31:40 -07:00
|
|
|
NotFound,
|
2020-03-31 06:58:48 -07:00
|
|
|
Success
|
|
|
|
}
|
|
|
|
|
|
|
|
enum Source {
|
|
|
|
Url,
|
|
|
|
Input
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface Details {
|
|
|
|
executable: boolean;
|
|
|
|
owner: PublicKey;
|
|
|
|
space: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface Account {
|
|
|
|
id: number;
|
|
|
|
status: Status;
|
|
|
|
source: Source;
|
|
|
|
pubkey: PublicKey;
|
2020-04-05 01:31:40 -07:00
|
|
|
lamports?: number;
|
2020-03-31 06:58:48 -07:00
|
|
|
details?: Details;
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
export enum ActionType {
|
|
|
|
Update,
|
|
|
|
Input
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Update {
|
|
|
|
type: ActionType.Update;
|
2020-04-01 01:35:12 -07:00
|
|
|
address: string;
|
2020-03-31 06:58:48 -07:00
|
|
|
status: Status;
|
2020-04-05 01:31:40 -07:00
|
|
|
lamports?: number;
|
2020-03-31 06:58:48 -07:00
|
|
|
details?: Details;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Input {
|
|
|
|
type: ActionType.Input;
|
|
|
|
pubkey: PublicKey;
|
|
|
|
}
|
|
|
|
|
|
|
|
type Action = Update | Input;
|
|
|
|
type Dispatch = (action: Action) => void;
|
|
|
|
|
|
|
|
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,
|
|
|
|
source: Source.Input,
|
|
|
|
pubkey: action.pubkey
|
|
|
|
}
|
|
|
|
};
|
|
|
|
return { ...state, accounts, idCounter };
|
|
|
|
}
|
|
|
|
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,
|
|
|
|
status: action.status,
|
2020-04-05 01:31:40 -07:00
|
|
|
details: action.details,
|
|
|
|
lamports: action.lamports
|
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-05 10:34:04 -07:00
|
|
|
export const ACCOUNT_PATHS = ["account", "accounts", "address", "addresses"];
|
|
|
|
|
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 => {
|
|
|
|
const params = findGetParameter(path)?.split(",") || [];
|
|
|
|
const segments = findPathSegment(path)?.split(",") || [];
|
|
|
|
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,
|
|
|
|
source: Source.Url,
|
|
|
|
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,
|
|
|
|
status: Status.Checking,
|
2020-04-01 01:35:12 -07:00
|
|
|
address
|
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-01 01:35:12 -07:00
|
|
|
const result = await new Connection(url).getAccountInfo(
|
|
|
|
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
|
|
|
|
};
|
|
|
|
status = Status.Success;
|
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-05 01:31:40 -07:00
|
|
|
dispatch({ type: ActionType.Update, status, lamports, details, 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
|
|
|
|
)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function useAccountsDispatch() {
|
|
|
|
const context = React.useContext(DispatchContext);
|
|
|
|
if (!context) {
|
|
|
|
throw new Error(
|
|
|
|
`useAccountsDispatch must be used within a AccountsProvider`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return context;
|
|
|
|
}
|