Show account tx history
This commit is contained in:
parent
73922609e4
commit
e5f69673d7
|
@ -1269,9 +1269,8 @@
|
||||||
"integrity": "sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw=="
|
"integrity": "sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw=="
|
||||||
},
|
},
|
||||||
"@solana/web3.js": {
|
"@solana/web3.js": {
|
||||||
"version": "0.43.0",
|
"version": "github:jstarry/solana-web3.js#c7d8fe33877a9eae354b456e019ad5044c25eeb7",
|
||||||
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.43.0.tgz",
|
"from": "github:jstarry/solana-web3.js#address-history",
|
||||||
"integrity": "sha512-Zf+UfoAuXdn8WwvYSVb8k4hQ484L2ZK6eXlW5EN048273Nks0+Sf44QrUpq2AhYVm6KAT7siyB8Nhgo0jQAG6A==",
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.3.1",
|
"@babel/runtime": "^7.3.1",
|
||||||
"bn.js": "^5.0.0",
|
"bn.js": "^5.0.0",
|
||||||
|
@ -1296,9 +1295,9 @@
|
||||||
"integrity": "sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA=="
|
"integrity": "sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA=="
|
||||||
},
|
},
|
||||||
"buffer": {
|
"buffer": {
|
||||||
"version": "5.5.0",
|
"version": "5.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz",
|
||||||
"integrity": "sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==",
|
"integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"base64-js": "^1.0.2",
|
"base64-js": "^1.0.2",
|
||||||
"ieee754": "^1.1.4"
|
"ieee754": "^1.1.4"
|
||||||
|
@ -1627,9 +1626,9 @@
|
||||||
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="
|
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="
|
||||||
},
|
},
|
||||||
"@types/express-serve-static-core": {
|
"@types/express-serve-static-core": {
|
||||||
"version": "4.17.3",
|
"version": "4.17.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.5.tgz",
|
||||||
"integrity": "sha512-sHEsvEzjqN+zLbqP+8OXTipc10yH1QLR+hnr5uw29gi9AhCAAAdri8ClNV7iMdrJrIzXIQtlkPvq8tJGhj3QJQ==",
|
"integrity": "sha512-578YH5Lt88AKoADy0b2jQGwJtrBxezXtVe/MBqWXKZpqx91SnC0pVkVCcxcytz3lWW+cHBYDi3Ysh0WXc+rAYw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"@types/range-parser": "*"
|
"@types/range-parser": "*"
|
||||||
|
@ -1686,9 +1685,9 @@
|
||||||
"integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA=="
|
"integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA=="
|
||||||
},
|
},
|
||||||
"@types/lodash": {
|
"@types/lodash": {
|
||||||
"version": "4.14.149",
|
"version": "4.14.150",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.150.tgz",
|
||||||
"integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ=="
|
"integrity": "sha512-kMNLM5JBcasgYscD9x/Gvr6lTAv2NVgsKtet/hm93qMyf/D1pt+7jeEZklKJKxMVmXjxbRVQQGfqDSfipYCO6w=="
|
||||||
},
|
},
|
||||||
"@types/minimatch": {
|
"@types/minimatch": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
|
@ -7205,9 +7204,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"jayson": {
|
"jayson": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/jayson/-/jayson-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/jayson/-/jayson-3.2.1.tgz",
|
||||||
"integrity": "sha512-DZQnwA57GcStw4soSYB2VntWXFfoWvmSarlaWePDYOWhjxT72PBM4atEBomaTaS1uqk3jFC9UO9AyWjlujo3xw==",
|
"integrity": "sha512-8YfxxjQdcSVMr0/+7B1+aGUFAI+yQYfTWBOI+rq+PBu35HL7nJ3wayC2fTXeUJZqad/I4UoDwMtBTNVWJ9rzlg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/connect": "^3.4.32",
|
"@types/connect": "^3.4.32",
|
||||||
"@types/express-serve-static-core": "^4.16.9",
|
"@types/express-serve-static-core": "^4.16.9",
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@solana/web3.js": "^0.43.0",
|
"@solana/web3.js": "github:jstarry/solana-web3.js#address-history",
|
||||||
"@testing-library/jest-dom": "^4.2.4",
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
"@testing-library/react": "^9.3.2",
|
"@testing-library/react": "^9.3.2",
|
||||||
"@testing-library/user-event": "^7.1.2",
|
"@testing-library/user-event": "^7.1.2",
|
||||||
|
|
|
@ -8,13 +8,19 @@ import {
|
||||||
useTransactions,
|
useTransactions,
|
||||||
ActionType
|
ActionType
|
||||||
} from "./providers/transactions";
|
} from "./providers/transactions";
|
||||||
import { AccountsProvider } from "./providers/accounts";
|
import {
|
||||||
|
AccountsProvider,
|
||||||
|
useSelectedAccount,
|
||||||
|
useAccountsDispatch,
|
||||||
|
ActionType as AccountActionType
|
||||||
|
} from "./providers/accounts";
|
||||||
import { BlocksProvider } from "./providers/blocks";
|
import { BlocksProvider } from "./providers/blocks";
|
||||||
import ClusterStatusButton from "./components/ClusterStatusButton";
|
import ClusterStatusButton from "./components/ClusterStatusButton";
|
||||||
import AccountsCard from "./components/AccountsCard";
|
import AccountsCard from "./components/AccountsCard";
|
||||||
import TransactionsCard from "./components/TransactionsCard";
|
import TransactionsCard from "./components/TransactionsCard";
|
||||||
import ClusterModal from "./components/ClusterModal";
|
import ClusterModal from "./components/ClusterModal";
|
||||||
import TransactionModal from "./components/TransactionModal";
|
import TransactionModal from "./components/TransactionModal";
|
||||||
|
import AccountModal from "./components/AccountModal";
|
||||||
import Logo from "./img/logos-solana/light-explorer-logo.svg";
|
import Logo from "./img/logos-solana/light-explorer-logo.svg";
|
||||||
import { useCurrentTab, Tab } from "./providers/tab";
|
import { useCurrentTab, Tab } from "./providers/tab";
|
||||||
|
|
||||||
|
@ -31,6 +37,7 @@ function App() {
|
||||||
onClose={() => setShowClusterModal(false)}
|
onClose={() => setShowClusterModal(false)}
|
||||||
/>
|
/>
|
||||||
<TransactionModal />
|
<TransactionModal />
|
||||||
|
<AccountModal />
|
||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
<nav className="navbar navbar-expand-xl navbar-light">
|
<nav className="navbar navbar-expand-xl navbar-light">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
|
@ -120,15 +127,18 @@ type OverlayProps = {
|
||||||
|
|
||||||
function Overlay({ show, onClick }: OverlayProps) {
|
function Overlay({ show, onClick }: OverlayProps) {
|
||||||
const { selected } = useTransactions();
|
const { selected } = useTransactions();
|
||||||
const dispatch = useTransactionsDispatch();
|
const selectedAccount = useSelectedAccount();
|
||||||
|
const txDispatch = useTransactionsDispatch();
|
||||||
|
const accountDispatch = useAccountsDispatch();
|
||||||
|
|
||||||
if (show || !!selected)
|
if (show || !!selected || !!selectedAccount)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="modal-backdrop fade show"
|
className="modal-backdrop fade show"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onClick();
|
onClick();
|
||||||
dispatch({ type: ActionType.Deselect });
|
txDispatch({ type: ActionType.Deselect });
|
||||||
|
accountDispatch({ type: AccountActionType.Select });
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,147 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
useSelectedAccount,
|
||||||
|
useAccountsDispatch,
|
||||||
|
ActionType,
|
||||||
|
Account,
|
||||||
|
Status
|
||||||
|
} from "../providers/accounts";
|
||||||
|
import { TransactionError } from "@solana/web3.js";
|
||||||
|
|
||||||
|
function AccountModal() {
|
||||||
|
const selected = useSelectedAccount();
|
||||||
|
const dispatch = useAccountsDispatch();
|
||||||
|
const onClose = () => dispatch({ type: ActionType.Select });
|
||||||
|
const show = !!selected;
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (!selected) return null;
|
||||||
|
return (
|
||||||
|
<div className="modal-dialog modal-dialog-centered">
|
||||||
|
<div className="modal-content" onClick={e => e.stopPropagation()}>
|
||||||
|
<div className="modal-card card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h4 className="card-header-title">Account Transaction History</h4>
|
||||||
|
<button type="button" className="close" onClick={onClose}>
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<AccountDetails account={selected} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`modal fade${show ? " show" : ""}`} onClick={onClose}>
|
||||||
|
{renderContent()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccountDetails({ account }: { account: Account }) {
|
||||||
|
const renderError = (content: React.ReactNode) => {
|
||||||
|
return <span className="text-info">{content}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (account.status === Status.FetchingHistory) {
|
||||||
|
return renderError(
|
||||||
|
<>
|
||||||
|
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||||
|
Loading
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account.history === undefined)
|
||||||
|
return renderError("Failed to fetch account transaction history");
|
||||||
|
|
||||||
|
if (account.history.size === 0) return renderError("No transactions found");
|
||||||
|
|
||||||
|
const detailsList: React.ReactNode[] = [];
|
||||||
|
account.history.forEach((slotTransactions, slot) => {
|
||||||
|
detailsList.push(
|
||||||
|
<SlotTransactionDetails
|
||||||
|
slot={slot}
|
||||||
|
statuses={Array.from(slotTransactions.entries())}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{detailsList.map((details, i) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={++i}>
|
||||||
|
{i > 1 ? <hr className="mt-0 mx-n4 mb-4"></hr> : null}
|
||||||
|
{details}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SlotTransactionDetails({
|
||||||
|
slot,
|
||||||
|
statuses
|
||||||
|
}: {
|
||||||
|
slot: number;
|
||||||
|
statuses: [string, TransactionError | null][];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4 className="slot-pill">Slot #{slot}</h4>
|
||||||
|
<div className="list-group list-group-flush">
|
||||||
|
{statuses.map(([signature, err]) => {
|
||||||
|
return (
|
||||||
|
<ListGroupItem
|
||||||
|
key={signature}
|
||||||
|
signature={signature}
|
||||||
|
failed={err !== null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListGroupItem({
|
||||||
|
signature,
|
||||||
|
failed
|
||||||
|
}: {
|
||||||
|
signature: string;
|
||||||
|
failed: boolean;
|
||||||
|
}) {
|
||||||
|
let badgeText, badgeColor;
|
||||||
|
if (failed) {
|
||||||
|
badgeText = "Error";
|
||||||
|
badgeColor = "danger";
|
||||||
|
} else {
|
||||||
|
badgeText = "Success";
|
||||||
|
badgeColor = "primary";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="list-group-item slot-item">
|
||||||
|
<div className="row align-items-center justify-content-between flex-nowrap">
|
||||||
|
<div className="col-auto">
|
||||||
|
<span className={`badge badge-soft-${badgeColor} badge-pill`}>
|
||||||
|
{badgeText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="col min-width-0">
|
||||||
|
<h5 className="mb-0 text-truncate">
|
||||||
|
<code>{signature}</code>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AccountModal;
|
|
@ -2,6 +2,7 @@ import React from "react";
|
||||||
import {
|
import {
|
||||||
useAccounts,
|
useAccounts,
|
||||||
useAccountsDispatch,
|
useAccountsDispatch,
|
||||||
|
Dispatch,
|
||||||
fetchAccountInfo,
|
fetchAccountInfo,
|
||||||
ActionType,
|
ActionType,
|
||||||
Account,
|
Account,
|
||||||
|
@ -55,6 +56,7 @@ function AccountsCard() {
|
||||||
<th className="text-muted">Balance (SOL)</th>
|
<th className="text-muted">Balance (SOL)</th>
|
||||||
<th className="text-muted">Data (bytes)</th>
|
<th className="text-muted">Data (bytes)</th>
|
||||||
<th className="text-muted">Owner</th>
|
<th className="text-muted">Owner</th>
|
||||||
|
<th className="text-muted">Details</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="list">
|
<tbody className="list">
|
||||||
|
@ -86,8 +88,9 @@ function AccountsCard() {
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
<td>-</td>
|
<td>-</td>
|
||||||
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
{accounts.map(account => renderAccountRow(account))}
|
{accounts.map(account => renderAccountRow(account, dispatch, url))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -107,7 +110,11 @@ const renderHeader = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAccountRow = (account: Account) => {
|
const renderAccountRow = (
|
||||||
|
account: Account,
|
||||||
|
dispatch: Dispatch,
|
||||||
|
url: string
|
||||||
|
) => {
|
||||||
let statusText;
|
let statusText;
|
||||||
let statusClass;
|
let statusClass;
|
||||||
switch (account.status) {
|
switch (account.status) {
|
||||||
|
@ -116,10 +123,12 @@ const renderAccountRow = (account: Account) => {
|
||||||
statusText = "Not Found";
|
statusText = "Not Found";
|
||||||
break;
|
break;
|
||||||
case Status.CheckFailed:
|
case Status.CheckFailed:
|
||||||
|
case Status.HistoryFailed:
|
||||||
statusClass = "danger";
|
statusClass = "danger";
|
||||||
statusText = "Error";
|
statusText = "Error";
|
||||||
break;
|
break;
|
||||||
case Status.Checking:
|
case Status.Checking:
|
||||||
|
case Status.FetchingHistory:
|
||||||
statusClass = "info";
|
statusClass = "info";
|
||||||
statusText = "Fetching";
|
statusText = "Fetching";
|
||||||
break;
|
break;
|
||||||
|
@ -148,6 +157,42 @@ const renderAccountRow = (account: Account) => {
|
||||||
balance = `◎${(1.0 * account.lamports) / LAMPORTS_PER_SOL}`;
|
balance = `◎${(1.0 * account.lamports) / LAMPORTS_PER_SOL}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderDetails = () => {
|
||||||
|
let onClick, icon;
|
||||||
|
switch (account.status) {
|
||||||
|
case Status.Success:
|
||||||
|
icon = "more-horizontal";
|
||||||
|
onClick = () =>
|
||||||
|
dispatch({
|
||||||
|
type: ActionType.Select,
|
||||||
|
address: account.pubkey.toBase58()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Status.CheckFailed:
|
||||||
|
case Status.HistoryFailed: {
|
||||||
|
icon = "refresh-cw";
|
||||||
|
onClick = () => {
|
||||||
|
fetchAccountInfo(dispatch, account.pubkey.toBase58(), url);
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="btn btn-rounded-circle btn-white btn-sm"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<span className={`fe fe-${icon}`}></span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const base58AccountPubkey = account.pubkey.toBase58();
|
const base58AccountPubkey = account.pubkey.toBase58();
|
||||||
return (
|
return (
|
||||||
<tr key={account.id}>
|
<tr key={account.id}>
|
||||||
|
@ -165,6 +210,7 @@ const renderAccountRow = (account: Account) => {
|
||||||
<td>{balance}</td>
|
<td>{balance}</td>
|
||||||
<td>{data}</td>
|
<td>{data}</td>
|
||||||
<td>{owner === "-" ? owner : <code>{owner}</code>}</td>
|
<td>{owner === "-" ? owner : <code>{owner}</code>}</td>
|
||||||
|
<td>{renderDetails()}</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,15 +1,28 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { PublicKey, Connection } from "@solana/web3.js";
|
import {
|
||||||
|
PublicKey,
|
||||||
|
Connection,
|
||||||
|
TransactionSignature,
|
||||||
|
TransactionError,
|
||||||
|
SignatureStatus
|
||||||
|
} from "@solana/web3.js";
|
||||||
import { findGetParameter, findPathSegment } from "../utils/url";
|
import { findGetParameter, findPathSegment } from "../utils/url";
|
||||||
import { useCluster, ClusterStatus } from "./cluster";
|
import { useCluster, ClusterStatus } from "./cluster";
|
||||||
|
|
||||||
export enum Status {
|
export enum Status {
|
||||||
Checking,
|
Checking,
|
||||||
CheckFailed,
|
CheckFailed,
|
||||||
|
FetchingHistory,
|
||||||
|
HistoryFailed,
|
||||||
NotFound,
|
NotFound,
|
||||||
Success
|
Success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type History = Map<
|
||||||
|
number,
|
||||||
|
Map<TransactionSignature, TransactionError | null>
|
||||||
|
>;
|
||||||
|
|
||||||
enum Source {
|
enum Source {
|
||||||
Url,
|
Url,
|
||||||
Input
|
Input
|
||||||
|
@ -23,30 +36,36 @@ export interface Details {
|
||||||
|
|
||||||
export interface Account {
|
export interface Account {
|
||||||
id: number;
|
id: number;
|
||||||
status: Status;
|
|
||||||
source: Source;
|
|
||||||
pubkey: PublicKey;
|
pubkey: PublicKey;
|
||||||
|
source: Source;
|
||||||
|
status: Status;
|
||||||
lamports?: number;
|
lamports?: number;
|
||||||
details?: Details;
|
details?: Details;
|
||||||
|
history?: History;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Accounts = { [address: string]: Account };
|
type Accounts = { [address: string]: Account };
|
||||||
interface State {
|
interface State {
|
||||||
idCounter: number;
|
idCounter: number;
|
||||||
accounts: Accounts;
|
accounts: Accounts;
|
||||||
|
selected?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ActionType {
|
export enum ActionType {
|
||||||
Update,
|
Update,
|
||||||
Input
|
Input,
|
||||||
|
Select
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Update {
|
interface Update {
|
||||||
type: ActionType.Update;
|
type: ActionType.Update;
|
||||||
address: string;
|
address: string;
|
||||||
status: Status;
|
data: {
|
||||||
lamports?: number;
|
status: Status;
|
||||||
details?: Details;
|
lamports?: number;
|
||||||
|
details?: Details;
|
||||||
|
history?: History;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Input {
|
interface Input {
|
||||||
|
@ -54,7 +73,12 @@ interface Input {
|
||||||
pubkey: PublicKey;
|
pubkey: PublicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action = Update | Input;
|
interface Select {
|
||||||
|
type: ActionType.Select;
|
||||||
|
address?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action = Update | Input | Select;
|
||||||
export type Dispatch = (action: Action) => void;
|
export type Dispatch = (action: Action) => void;
|
||||||
|
|
||||||
function reducer(state: State, action: Action): State {
|
function reducer(state: State, action: Action): State {
|
||||||
|
@ -74,14 +98,17 @@ function reducer(state: State, action: Action): State {
|
||||||
};
|
};
|
||||||
return { ...state, accounts, idCounter };
|
return { ...state, accounts, idCounter };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case ActionType.Select: {
|
||||||
|
return { ...state, selected: action.address };
|
||||||
|
}
|
||||||
|
|
||||||
case ActionType.Update: {
|
case ActionType.Update: {
|
||||||
let account = state.accounts[action.address];
|
let account = state.accounts[action.address];
|
||||||
if (account) {
|
if (account) {
|
||||||
account = {
|
account = {
|
||||||
...account,
|
...account,
|
||||||
status: action.status,
|
...action.data
|
||||||
details: action.details,
|
|
||||||
lamports: action.lamports
|
|
||||||
};
|
};
|
||||||
const accounts = {
|
const accounts = {
|
||||||
...state.accounts,
|
...state.accounts,
|
||||||
|
@ -167,8 +194,10 @@ export async function fetchAccountInfo(
|
||||||
) {
|
) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionType.Update,
|
type: ActionType.Update,
|
||||||
status: Status.Checking,
|
address,
|
||||||
address
|
data: {
|
||||||
|
status: Status.Checking
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let status;
|
let status;
|
||||||
|
@ -188,13 +217,60 @@ export async function fetchAccountInfo(
|
||||||
executable: result.executable,
|
executable: result.executable,
|
||||||
owner: result.owner
|
owner: result.owner
|
||||||
};
|
};
|
||||||
status = Status.Success;
|
status = Status.FetchingHistory;
|
||||||
|
fetchAccountHistory(dispatch, address, url);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch account info", error);
|
console.error("Failed to fetch account info", error);
|
||||||
status = Status.CheckFailed;
|
status = Status.CheckFailed;
|
||||||
}
|
}
|
||||||
dispatch({ type: ActionType.Update, status, lamports, details, address });
|
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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAccounts() {
|
export function useAccounts() {
|
||||||
|
@ -210,6 +286,18 @@ export function useAccounts() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
export function useAccountsDispatch() {
|
export function useAccountsDispatch() {
|
||||||
const context = React.useContext(DispatchContext);
|
const context = React.useContext(DispatchContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
|
|
|
@ -65,6 +65,33 @@ h4.ix-pill {
|
||||||
background-color: theme-color-level(info, $badge-soft-bg-level);
|
background-color: theme-color-level(info, $badge-soft-bg-level);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-flush .list-group-item.ix-item:first-child {
|
h4.slot-pill {
|
||||||
border-top-width: 1px;
|
display: inline-block;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
margin-left: -5px;
|
||||||
|
background-color: theme-color-level(primary, $badge-soft-bg-level);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:last-child {
|
||||||
|
&.ix-item, &.slot-item {
|
||||||
|
border-bottom-width: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group:last-child .list-group-item:last-child {
|
||||||
|
&.ix-item, &.slot-item {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:first-child {
|
||||||
|
&.ix-item, &.slot-item {
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.min-width-0 {
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
Loading…
Reference in New Issue