diff --git a/packages/cacti-ledger-browser/package.json b/packages/cacti-ledger-browser/package.json index 1d78499788..ef98b7be88 100644 --- a/packages/cacti-ledger-browser/package.json +++ b/packages/cacti-ledger-browser/package.json @@ -58,6 +58,7 @@ "@mui/icons-material": "5.15.10", "@mui/material": "5.15.15", "@supabase/supabase-js": "1.35.6", + "@tanstack/react-query": "5.29.2", "apexcharts": "3.45.2", "localforage": "1.10.0", "match-sorter": "6.3.3", @@ -70,6 +71,8 @@ "web3": "4.1.1" }, "devDependencies": { + "@tanstack/eslint-plugin-query": "5.28.11", + "@tanstack/react-query-devtools": "5.29.2", "@types/react": "18.2.43", "@types/react-dom": "18.2.17", "@types/sort-by": "1", diff --git a/packages/cacti-ledger-browser/src/main/typescript/CactiLedgerBrowserApp.tsx b/packages/cacti-ledger-browser/src/main/typescript/CactiLedgerBrowserApp.tsx index 237e70bb8d..457b71c24a 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/CactiLedgerBrowserApp.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/CactiLedgerBrowserApp.tsx @@ -1,6 +1,7 @@ import { useRoutes, BrowserRouter, RouteObject } from "react-router-dom"; import CssBaseline from "@mui/material/CssBaseline"; import { ThemeProvider, createTheme } from "@mui/material/styles"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { themeOptions } from "./theme"; import ContentLayout from "./components/Layout/ContentLayout"; @@ -105,14 +106,21 @@ const App: React.FC = ({ appConfig }) => { ); }; +// MUI Theme const theme = createTheme(themeOptions); +// React Query client +const queryClient = new QueryClient(); + const CactiLedgerBrowserApp: React.FC = ({ appConfig }) => { return ( - - + + + + {/* */} + ); diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/cacti/pages/status-page.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/cacti/pages/status-page.tsx index 22c93c7bb9..ed71b44257 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/cacti/pages/status-page.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/cacti/pages/status-page.tsx @@ -1,39 +1,15 @@ -import { useEffect, useState } from "react"; -import { supabase } from "../../../common/supabase-client"; import CardWrapper from "../../../components/ui/CardWrapper"; +import { useQuery } from "@tanstack/react-query"; +import { persistencePluginStatusQuery } from "../queries"; function StatusPage() { - const [getPluginStatus, setPluginStatuse] = useState([]); - - const fetchPluginStatus = async () => { - try { - const { data, error } = await supabase.from("plugin_status").select(); - if (error) { - throw new Error( - `Could not get plugin statuses from the DB: ${error.message}`, - ); - } - - if (data) { - setPluginStatuse( - data.map((p) => { - return { - ...p, - is_schema_initialized: p.is_schema_initialized - ? "Setup complete" - : "No schema", - }; - }), - ); - } - } catch (error) { - console.error("Error when fetching plugin statuses:", error); - } - }; + const { isSuccess, isError, data, error } = useQuery( + persistencePluginStatusQuery(), + ); - useEffect(() => { - fetchPluginStatus(); - }, []); + if (isError) { + console.error("Data fetch error:", error); + } return (
@@ -49,7 +25,18 @@ function StatusPage() { ], } as any } - data={getPluginStatus} + data={ + isSuccess + ? (data as any).map((p: any) => { + return { + ...p, + is_schema_initialized: p.is_schema_initialized + ? "Setup complete" + : "No schema", + }; + }) + : [] + } title={"Persistence Plugins"} display={"All"} trimmed={false} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/cacti/queries.ts b/packages/cacti-ledger-browser/src/main/typescript/apps/cacti/queries.ts new file mode 100644 index 0000000000..321dce4abf --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/cacti/queries.ts @@ -0,0 +1,5 @@ +import { supabaseQueryTable } from "../../common/supabase-client"; + +export function persistencePluginStatusQuery() { + return supabaseQueryTable("plugin_status"); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/TokenHeader/TokenHeader.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/TokenHeader/TokenHeader.tsx index c3d2b6d59e..62c154cdfa 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/TokenHeader/TokenHeader.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/components/TokenHeader/TokenHeader.tsx @@ -1,46 +1,33 @@ import TokenAccount from "./TokenAccount"; import styles from "./TokenHeader.module.css"; -import { useEffect, useState } from "react"; +import { ethTokenDetails } from "../../queries"; +import { useQuery } from "@tanstack/react-query"; import { TokenMetadata20 } from "../../../../common/supabase-types"; -import { supabase } from "../../../../common/supabase-client"; -function TokenHeader(props: Record) { - const [tokenData, setTokenData] = useState(); +function TokenHeader(props: { accountNum: string; tokenAddress: string }) { + const { isError, data, error } = useQuery( + ethTokenDetails("erc20", props.tokenAddress), + ); - const fetchData = async () => { - try { - const { data } = await supabase - .from(`token_metadata_erc20`) - .select("*") - .match({ address: props.token_address }); - console.log(data); - if (data?.[0]) { - setTokenData(data[0]); - } else { - throw new Error("Failed to load token details"); - } - } catch (error: any) { - console.error(error.message); - } - }; + if (isError) { + console.error("Token header fetch error:", error); + } - useEffect(() => { - fetchData(); - }, []); + console.log(data); return (

- Address: {props.token_address} + Address: {props.tokenAddress}

- Created at: {tokenData?.created_at} + Created at: {data?.created_at}

Total supply: - {tokenData?.total_supply} + {(data as TokenMetadata20).total_supply}

diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/index.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/index.tsx index 1810090129..7a61779bcb 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/index.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/index.tsx @@ -4,7 +4,6 @@ import Dashboard from "./pages/Dashboard/Dashboard"; import Blocks from "./pages/Blocks/Blocks"; import Transactions from "./pages/Transactions/Transactions"; import Accounts from "./pages/Accounts/Accounts"; -import TokenTransactionDetails from "./pages/Details/TokenTransactionDetails"; import TransactionDetails from "./pages/Details/TransactionDetails"; import ERC20 from "./pages/ERC20/ERC20"; import SingleTokenHistory from "./pages/SingleTokenHistory/SingleTokenHistory"; @@ -61,16 +60,6 @@ const ethConfig: AppConfig = { }, ], }, - { - path: "token-txn-details", - element: , - children: [ - { - path: ":standard/:address", - element: , - }, - ], - }, { path: "token-details", element: , diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Accounts/Accounts.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Accounts/Accounts.tsx index 4a84b417e3..e8254d8ecb 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Accounts/Accounts.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Accounts/Accounts.tsx @@ -1,14 +1,24 @@ -import { supabase } from "../../../../common/supabase-client"; import CardWrapper from "../../../../components/ui/CardWrapper"; import { useNavigate, useParams } from "react-router-dom"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { ethGetTokenOwners } from "../../queries"; function Accounts() { const params = useParams(); + if (typeof params.standard === "undefined") { + throw new Error(`Accounts called with empty token standard ${params}`); + } const navigate = useNavigate(); - const [accounts, setAccounts] = useState<{ address: string }[]>([]); + const { isError, data, error } = useQuery( + ethGetTokenOwners(params.standard.toLowerCase()), + ); const [searchKey, setSearchKey] = useState(""); + if (isError) { + console.error("Token owners fetch error:", error); + } + const tableProps = { onClick: { action: (param: string) => navigate(`/eth/${params.standard}/${param}`), @@ -22,36 +32,13 @@ function Accounts() { ], }; - const fetchAccounts = async () => { - try { - const { data, error } = await supabase - .from(`token_${params.standard?.toLowerCase()}`) - .select("account_address"); - if (data) { - const objData = [...new Set(data.map((el) => el.account_address))].map( - (el) => ({ address: el }), - ); - setAccounts(objData); - } - if (error) { - console.error(error.message); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fetchAccounts(); - }, []); - return (
setSearchKey(e)} diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Blocks/Blocks.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Blocks/Blocks.tsx index dbafa5e62f..971cd4b14d 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Blocks/Blocks.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Blocks/Blocks.tsx @@ -1,16 +1,19 @@ -import { supabase } from "../../../../common/supabase-client"; import { useNavigate } from "react-router-dom"; import CardWrapper from "../../../../components/ui/CardWrapper"; import styles from "./Blocks.module.css"; -import { useEffect, useState } from "react"; -import { Block } from "web3"; +import { useQuery } from "@tanstack/react-query"; +import { ethereumAllBlocksQuery } from "../../queries"; type ObjectKey = keyof typeof styles; function Blocks() { const navigate = useNavigate(); - const [block, setBlock] = useState([]); + const { isError, data, error } = useQuery(ethereumAllBlocksQuery()); + + if (isError) { + console.error("Transactions fetch error:", error); + } const blocksTableProps = { onClick: { @@ -24,30 +27,11 @@ function Blocks() { ], }; - const fetchBlock = async () => { - try { - const { data, error } = await supabase.from("block").select("*"); - if (data) { - console.log(JSON.stringify(data)); - setBlock(data); - } - if (error) { - console.error(error.message); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fetchBlock(); - }, []); - return (
([]); - const [block, setBlock] = useState([]); - - const txnTableProps = { - onClick: { - action: (param: string) => { - navigate(`/eth/txn-details/${param}`); - }, - prop: "id", - }, - schema: [ - { display: "transaction id", objProp: ["id"] }, - { display: "sender/recipient", objProp: ["from", "to"] }, - { display: "token value", objProp: ["eth_value"] }, - ], - }; - - const blocksTableProps = { - onClick: { - action: (param: string) => { - navigate(`/eth/block-details/${param}`); - }, - prop: "number", - }, - schema: [ - { display: "created at", objProp: ["created_at"] }, - { display: "block number", objProp: ["number"] }, - { display: "hash", objProp: ["hash"] }, - ], - }; - const fetchTransactions = async () => { - try { - const { data, error } = await supabase.from("transaction").select("*"); - if (data) { - setTransaction(data); - } - if (error) { - console.error(error.message); - } - } catch (error: any) { - console.error(error.message); - } - }; - - const fetchBlock = async () => { - try { - const { data, error } = await supabase.from("block").select("*"); - if (data) { - setBlock(data); - } - if (error) { - console.error(error.message); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fetchTransactions(); - fetchBlock(); - }, []); - return (

Dashboard

- - + +
); diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Details/BlockDetails.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Details/BlockDetails.tsx index 51871a1aad..e82ae5cacc 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Details/BlockDetails.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Details/BlockDetails.tsx @@ -1,59 +1,47 @@ -import { supabase } from "../../../../common/supabase-client"; -import { Block } from "../../../../common/supabase-types"; - import styles from "./Details.module.css"; import { useParams } from "react-router-dom"; -import { useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { ethereumBlockByNumber } from "../../queries"; function BlockDetails() { - const [details, setDetails] = useState(); const params = useParams(); + if (typeof params.number === "undefined") { + throw new Error(`BlockDetails called with empty block number ${params}`); + } + const { isSuccess, isError, data, error } = useQuery({ + ...ethereumBlockByNumber(params.number), + staleTime: Infinity, + }); - const fethcData = async () => { - try { - const { data } = await supabase - .from("block") - .select("*") - .match({ number: params.number }); - if (data?.[0]) { - setDetails(data[0]); - } else { - throw new Error("Failed to load block details"); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fethcData(); - }, []); + if (isError) { + console.error("Data fetch error:", error); + } return (
- {details ? ( + {isSuccess ? ( <>

Block Details

- Address: {details.number}{" "} + Address: {data.number}{" "}

{" "} Created at: - {details.created_at} + {data.created_at}

Hash: - {details.hash} + {data.hash}

Number of transaction: - {details.number_of_tx} + {data.number_of_tx}

Sync at: - {details.sync_at} + {data.sync_at}

) : ( diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Details/TokenDetails.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Details/TokenDetails.tsx index ee10458166..4db7448293 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Details/TokenDetails.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Details/TokenDetails.tsx @@ -1,61 +1,47 @@ -import { supabase } from "../../../../common/supabase-client"; import { STANDARDS } from "../../../../common/token-standards"; -import { - TokenMetadata20, - TokenMetadata721, -} from "../../../../common/supabase-types"; import styles from "./Details.module.css"; -import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; +import { ethTokenDetails } from "../../queries"; +import { TokenMetadata20 } from "../../../../common/supabase-types"; const TokenDetails = () => { - const [tokenData, setTokenData] = useState< - TokenMetadata20 | TokenMetadata721 | any - >(); - const params = useParams(); + if ( + typeof params.standard === "undefined" || + typeof params.address === "undefined" + ) { + throw new Error(`Token details called with empty args ${params}`); + } + const { isError, data, error } = useQuery( + ethTokenDetails(params.standard.toLowerCase(), params.address), + ); - const fethcData = async () => { - try { - const { data } = await supabase - .from(`token_metadata_${params.standard?.toLowerCase()}`) - .select("*") - .match({ address: params.address }); - if (data?.[0]) { - setTokenData(data[0]); - } else { - throw new Error("Failed to load token details"); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fethcData(); - }, []); + if (isError) { + console.error("Token details fetch error:", error); + } return (

Token Details

- Adress: {tokenData?.address}{" "} + Adress: {data?.address}{" "}

Created at: - {tokenData?.created_at} + {data?.created_at}

Name: - {tokenData?.name} + {data?.name}

Symbol: - {tokenData?.symbol} + {data?.symbol}

{params.standard === STANDARDS.erc20 && ( -

total_supply : {tokenData?.total_supply}

+

total_supply : {(data as TokenMetadata20).total_supply}

)}
diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Details/TokenTransactionDetails.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Details/TokenTransactionDetails.tsx deleted file mode 100644 index 8cd1bc4b24..0000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Details/TokenTransactionDetails.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { supabase } from "../../../../common/supabase-client"; -import { STANDARDS } from "../../../../common/token-standards"; -import { ERC20Txn, ERC721Txn } from "../../../../common/supabase-types"; - -import styles from "./Details.module.css"; -import { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; - -const TokenTransactionDetails = () => { - const [txnData, setTxnData] = useState({}); - const params = useParams(); - - const fethcData = async () => { - try { - const { data } = await supabase - .from(`token_${params.standard?.toLowerCase()}`) - .select("*") - .match({ account_address: params.address }); - if (data?.[0]) { - setTxnData(data[0]); - } else { - throw new Error("Failed to load transaction details"); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fethcData(); - }, []); - - return ( -
-
-

Details of Transaction

-

- {" "} - Address: - {txnData?.account_address}{" "} -

-

- {" "} - Created_at: - {txnData?.token_address} -

- {params.standard === STANDARDS.erc20 && ( -

- {" "} - Balance: - {txnData()?.balance} -

- )} - {params.standard === STANDARDS.erc721 && ( -

- {" "} - Uri: - {txnData()?.uri} -

- )} -
-
- ); -}; - -export default TokenTransactionDetails; diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Details/TransactionDetails.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Details/TransactionDetails.tsx index 79f7fec79b..1a5a846f78 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Details/TransactionDetails.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Details/TransactionDetails.tsx @@ -1,15 +1,38 @@ -import { supabase } from "../../../../common/supabase-client"; import CardWrapper from "../../../../components/ui/CardWrapper"; -import { Transaction, TokenTransfer } from "../../../../common/supabase-types"; - import styles from "./Details.module.css"; -import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; +import { ethereumTokenTransfersByTxId, ethereumTxById } from "../../queries"; const TransactionsDetails = () => { - const [details, setDetails] = useState({}); - const [transfers, setTransfers] = useState([]); const params = useParams(); + if (typeof params.id === "undefined") { + throw new Error(`TransactionsDetails called with empty txId ${params}`); + } + const { + isError: txIsError, + data: txData, + error: txError, + } = useQuery({ + ...ethereumTxById(params.id), + staleTime: Infinity, + }); + + const { + isError: txTransfersIsError, + data: txTransfersData, + error: txTransfersError, + } = useQuery({ + ...ethereumTokenTransfersByTxId(params.id), + staleTime: Infinity, + }); + + if (txIsError) { + console.error("Transaction fetch error:", txError); + } + if (txTransfersIsError) { + console.error("Token transfers fetch error:", txTransfersError); + } const detailsTableProps = { onClick: { @@ -23,71 +46,34 @@ const TransactionsDetails = () => { ], }; - const fetchDetails = async () => { - try { - const { data } = await supabase - .from("transaction") - .select("*") - .match({ id: params.id }); - if (data?.[0]) { - setDetails(data[0]); - } else { - throw new Error("Failed to load transaction details"); - } - } catch (error: any) { - console.error(error.message); - } - }; - - const fetchTransfers = async () => { - try { - const { data } = await supabase - .from("token_transfer") - .select("*") - .match({ transaction_id: params.id }); - if (data) { - setTransfers(data); - } else { - throw new Error("Failed to load transfers"); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fetchDetails(); - fetchTransfers(); - }, []); - return (

Details of Transaction

{" "} - Hash: {details.hash}{" "} + Hash: {txData?.hash}{" "}

Block: - {details.block_number} + {txData?.block_number}

From: - {details.from} + {txData?.from}

To: - {details.to}{" "} + {txData?.to}{" "}

{" "} - Value:   {details.eth_value} + Value:   {txData?.eth_value}

{ const params = useParams(); + if (typeof params.account === "undefined") { + throw new Error(`ERC20 called with empty account address ${params}`); + } const navigate = useNavigate(); - const [token_erc20, setToken_erc20] = useState([]); + const { isError, data, error } = useQuery( + ethERC20TokensByOwner(params.account), + ); + if (isError) { + console.error("Data fetch error:", error); + } const ercTableProps = { onClick: { @@ -30,41 +37,20 @@ const ERC20 = () => { ], }; - const fetchERC20 = async () => { - try { - const { data, error } = await supabase - .from("token_erc20") - .select() - .eq("account_address", params.account); - if (data) { - setToken_erc20(data); - } - if (error) { - throw new Error(error.message); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fetchERC20(); - }, []); - return (
- +
); diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/ERC721/ERC721.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/ERC721/ERC721.tsx index d703a8aae4..6360cd04ce 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/ERC721/ERC721.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/ERC721/ERC721.tsx @@ -1,17 +1,35 @@ -import TokenAccount from "../../../../components/TokenHeader/TokenAccount"; -import { supabase } from "../../../../common/supabase-client"; import CardWrapper from "../../../../components/ui/CardWrapper"; import styles from "./ERC721.module.css"; import { useNavigate, useParams } from "react-router-dom"; -import { useEffect, useState } from "react"; -import { ERC721Txn, TokenMetadata721 } from "../../../../common/supabase-types"; +import { useQuery } from "@tanstack/react-query"; +import { ethAllERC721History, ethERC721TokensByTxId } from "../../queries"; +import TokenAccount from "../../components/TokenHeader/TokenAccount"; function ERC721() { const params = useParams(); + if (typeof params.account === "undefined") { + throw new Error(`ERC721 list called with empty account address ${params}`); + } const navigate = useNavigate(); - const [token_erc721, setToken_erc721] = useState([]); - const [tokenMetadata, setTokenMetadata] = useState([]); + const { + isError: isTokenListError, + data: tokenList, + error: tokenListError, + } = useQuery(ethERC721TokensByTxId(params.account)); + const { + isError: isTokenMetadataError, + data: tokenMetadata, + error: tokenMetadataError, + } = useQuery(ethAllERC721History()); + + if (isTokenListError) { + console.error("Token list for account fetch error:", tokenListError); + } + + if (isTokenMetadataError) { + console.error("Token metadata fetch error:", tokenMetadataError); + } const ercTableProps = { onClick: { @@ -50,49 +68,11 @@ function ERC721() { ], }; - const fetchERC721 = async () => { - try { - const { data, error } = await supabase - .from("erc721_txn_meta_view") - .select() - .eq("account_address", params.account); - if (data) { - setToken_erc721(data); - } - if (error) { - throw new Error(error.message); - } - } catch (error: any) { - console.error(error.message); - } - }; - - const fetchMetadata = async () => { - try { - const { data, error } = await supabase - .from(`erc721_token_history_view`) - .select("*"); - if (data) { - setTokenMetadata(data); - } - if (error) { - console.error(error.message); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fetchERC721(); - fetchMetadata(); - }, []); - return (
{ - type ObjectKey = keyof typeof styles; - const [transactions, setTransactions] = useState([]); - const [balanceHistory, setBalanceHistory] = useState([]); - const navigate = useNavigate(); const params = useParams(); + if ( + typeof params.address === "undefined" || + typeof params.account === "undefined" + ) { + throw new Error(`ERC20 called with empty token or owner address ${params}`); + } + const { + isError, + data: txData, + error, + } = useQuery(ethERC20TokensHistory(params.address, params.account)); - const tokenTableProps = { - onClick: { - action: (param: string) => navigate(`/view/${param}`), - prop: "id", + const { + data: balanceHistory, + isError: isBalanceHistoryError, + error: balanceHistoryError, + } = useQuery({ + queryKey: ["balanceHistory", txData], + queryFn: () => { + let balance = 0; + const balances = (txData ?? []).map((txn) => { + let txn_value = txn.value || 0; + if (txn.recipient !== params.account) { + txn_value *= -1; + } + balance += txn_value; + return { + created_at: txn.created_at + "Z", + balance: balance, + }; + }); + return balances; }, + enabled: !!txData, + }); + + if (isError) { + console.error("Token history fetch error:", error); + } + + if (isBalanceHistoryError) { + console.error("Balance history calculation error:", balanceHistoryError); + } + + const tokenTableProps = { schema: [ { display: "created at", @@ -45,56 +80,16 @@ const SingleTokenHistory = () => { ], }; - const calcTokenBalance = (txnData: TokenHistoryItem20[]) => { - let balance = 0; - const balances = txnData.map((txn) => { - let txn_value = txn.value || 0; - let account = params.account; - if (txn.recipient !== account) { - txn_value *= -1; - } - balance += txn_value; - return { - created_at: txn.created_at + "Z", - balance: balance, - }; - }); - return balances; - }; - - const fetchTransactions = async () => { - try { - const { data, error } = await supabase - .from("erc20_token_history_view") - .select("*") - .match({ token_address: params.address }) - .or(`sender.eq.${params.account}, recipient.eq.${params.account}`); - if (data) { - setTransactions(data); - setBalanceHistory(calcTokenBalance(data)); - } - if (error) { - console.error(error.message); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fetchTransactions(); - }, []); - return (
- +
- {transactions.length > 0 ? ( + {txData && txData.length > 0 ? ( <> { ) : ( )} - - {/* 0} - fallback={} - > - - - */}
); diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Transactions/Transactions.tsx b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Transactions/Transactions.tsx index 5cf6444f35..38716f9439 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Transactions/Transactions.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/pages/Transactions/Transactions.tsx @@ -1,14 +1,17 @@ -import { supabase } from "../../../../common/supabase-client"; -import { Transaction } from "../../../../common/supabase-types"; import CardWrapper from "../../../../components/ui/CardWrapper"; import styles from "./Transactions.module.css"; import { useNavigate } from "react-router-dom"; -import { useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { ethereumAllTransactionsQuery } from "../../queries"; function Transactions() { const navigate = useNavigate(); - const [transactions, setTransactions] = useState([]); + const { isError, data, error } = useQuery(ethereumAllTransactionsQuery()); + + if (isError) { + console.error("Transactions fetch error:", error); + } const txnTableProps = { onClick: { @@ -31,31 +34,13 @@ function Transactions() { ], }; - const fetchTransactions = async () => { - try { - const { data } = await supabase.from("transaction").select("*"); - if (data) { - console.log(JSON.stringify(data)); - setTransactions(data); - } else { - throw new Error("Failed to load transactions"); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fetchTransactions(); - }, []); - return (
diff --git a/packages/cacti-ledger-browser/src/main/typescript/apps/eth/queries.ts b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/queries.ts new file mode 100644 index 0000000000..061fabf102 --- /dev/null +++ b/packages/cacti-ledger-browser/src/main/typescript/apps/eth/queries.ts @@ -0,0 +1,126 @@ +import { queryOptions } from "@tanstack/react-query"; +import { + supabase, + supabaseQueryKey, + supabaseQueryTable, + supabaseQuerySingleMatchingEntry, + supabaseQueryAllMatchingEntries, +} from "../../common/supabase-client"; +import { + Transaction, + Block, + TokenTransfer, + ERC20Txn, + TokenHistoryItem20, + ERC721Txn, + TokenMetadata721, + TokenMetadata20, +} from "../../common/supabase-types"; + +export function ethereumAllTransactionsQuery() { + return supabaseQueryTable("transaction"); +} + +export function ethereumAllBlocksQuery() { + return supabaseQueryTable("block"); +} + +export function ethereumBlockByNumber(blockNumber: number | string) { + return supabaseQuerySingleMatchingEntry("block", { + number: blockNumber, + }); +} + +export function ethereumTxById(txId: number | string) { + return supabaseQuerySingleMatchingEntry("transaction", { + id: txId, + }); +} + +export function ethereumTokenTransfersByTxId(txId: number | string) { + return supabaseQueryAllMatchingEntries("token_transfer", { + transaction_id: txId, + }); +} + +export function ethGetTokenOwners(tokenStandard: string) { + if (!["erc20", "erc721"].includes(tokenStandard)) { + throw new Error(`Unknown token standard requested! ${tokenStandard}`); + } + const tableName = `token_${tokenStandard}`; + return queryOptions({ + queryKey: [supabaseQueryKey, tableName, "account_address"], + queryFn: async () => { + const { data, error } = await supabase + .from(tableName) + .select("account_address"); + if (error) { + throw new Error( + `Could not get token owners from '${tableName}' table: ${error.message}`, + ); + } + + // TODO - use stored procedure to return unique account list + return [...new Set(data.map((el) => el.account_address))].map((el) => ({ + address: el, + })); + }, + }); +} + +export function ethERC20TokensByOwner(accountAddress: number | string) { + return supabaseQueryAllMatchingEntries("token_erc20", { + account_address: accountAddress, + }); +} + +export function ethERC20TokensHistory( + tokenAddress: string, + tokenOwnerAddress: string, +) { + const tableName = "erc20_token_history_view"; + return queryOptions({ + queryKey: [supabaseQueryKey, tableName, tokenAddress, tokenOwnerAddress], + queryFn: async () => { + const { data, error } = await supabase + .from(tableName) + .select() + .match({ token_address: tokenAddress }) + .or( + `sender.eq.${tokenOwnerAddress}, recipient.eq.${tokenOwnerAddress}`, + ); + if (error) { + throw new Error( + `Could not get ERC20 [${tokenAddress}] of user ${tokenOwnerAddress} token history: ${error.message}`, + ); + } + + return data as TokenHistoryItem20[]; + }, + }); +} + +export function ethERC721TokensByTxId(accountAddress: string) { + return supabaseQueryAllMatchingEntries("erc721_txn_meta_view", { + account_address: accountAddress, + }); +} + +export function ethAllERC721History() { + return supabaseQueryTable("erc721_token_history_view"); +} + +export function ethTokenDetails(tokenStandard: string, tokenAddress: string) { + if (!["erc20", "erc721"].includes(tokenStandard)) { + throw new Error(`Unknown token standard requested! ${tokenStandard}`); + } + + const tableName = `token_metadata_${tokenStandard}`; + + return supabaseQuerySingleMatchingEntry( + tableName, + { + address: tokenAddress, + }, + ); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/common/supabase-client.tsx b/packages/cacti-ledger-browser/src/main/typescript/common/supabase-client.tsx index f0efcb4688..6a609c7e1f 100644 --- a/packages/cacti-ledger-browser/src/main/typescript/common/supabase-client.tsx +++ b/packages/cacti-ledger-browser/src/main/typescript/common/supabase-client.tsx @@ -1,7 +1,69 @@ import { createClient } from "@supabase/supabase-js"; +import { queryOptions } from "@tanstack/react-query"; +export const supabaseQueryKey = "supabase"; const supabaseUrl = "http://localhost:8000"; const supabaseKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"; - export const supabase = createClient(supabaseUrl, supabaseKey); + +/** + * React Query config to fetch entire table from supabase. + */ +export function supabaseQueryTable(tableName: string) { + return queryOptions({ + queryKey: [supabaseQueryKey, tableName], + queryFn: async () => { + const { data, error } = await supabase.from(tableName).select(); + if (error) { + throw new Error( + `Could not get data from '${tableName}' table: ${error.message}`, + ); + } + + return data as T; + }, + }); +} + +async function getMatchingTableEntries( + tableName: string, + query: Record, +) { + const { data, error } = await supabase.from(tableName).select().match(query); + if (error) { + throw new Error( + `Could not get data from '${tableName}' table using query '${query}': ${error.message}`, + ); + } + + return data; +} + +export function supabaseQueryAllMatchingEntries( + tableName: string, + query: Record, +) { + return queryOptions({ + queryKey: [supabaseQueryKey, tableName, query], + queryFn: () => { + return getMatchingTableEntries(tableName, query) as T; + }, + }); +} + +export function supabaseQuerySingleMatchingEntry( + tableName: string, + query: Record, +) { + return queryOptions({ + queryKey: [supabaseQueryKey, tableName, query], + queryFn: async () => { + const data = await getMatchingTableEntries(tableName, query); + if (data.length > 1) { + console.warn(`${tableName} query ${query} returned more than 1 entry!`); + } + return data[0] as T; + }, + }); +} diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/TokenHeader/TokenAccount.tsx b/packages/cacti-ledger-browser/src/main/typescript/components/TokenHeader/TokenAccount.tsx deleted file mode 100644 index 565880406c..0000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/components/TokenHeader/TokenAccount.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import styles from "./TokenHeader.module.css"; -import AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet"; - -function TokenAccount(props: any) { - return ( -
- - {" "} - - - {" "} - {props.accountNum} - -
- ); -} - -export default TokenAccount; diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/TokenHeader/TokenHeader.module.css b/packages/cacti-ledger-browser/src/main/typescript/components/TokenHeader/TokenHeader.module.css deleted file mode 100644 index 2d9ef9600c..0000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/components/TokenHeader/TokenHeader.module.css +++ /dev/null @@ -1,57 +0,0 @@ -.token-header { - display: flex; - flex-direction: column; - width: 100%; - gap: 1rem; -} - -.token-details { - width: 100%; - height: min-content; - border: 1px solid rgb(240, 236, 236); - border-radius: 10px; - gap: 3rem; - padding: 1rem 2rem; - display: flex; - justify-content: flex-start; - align-items: center; - background-color: rgb(247, 245, 245); -} - -.token-details div { - display: flex; - align-items: center; - gap: 1rem; -} - -.token-icon { - height: 100%; - transform: translateY(10%); - color: rgb(34, 70, 70); -} - -.token-account { - font-size: 16px; - width: 100%; - height: min-content; - display: flex; - align-items: center; - justify-content: center; - background-color: rgb(247, 245, 245); - border-radius: 10px; - padding: 1rem; - padding-left: 2rem; -} - -.token-account span { - display: flex; - align-items: center; - gap: .5rem; -} - -.token-account-icon { - color: rgb(22, 92, 65); - font-size: 28px; - height: 30px; - width: 30px; -} \ No newline at end of file diff --git a/packages/cacti-ledger-browser/src/main/typescript/components/TokenHeader/TokenHeader.tsx b/packages/cacti-ledger-browser/src/main/typescript/components/TokenHeader/TokenHeader.tsx deleted file mode 100644 index e333d84866..0000000000 --- a/packages/cacti-ledger-browser/src/main/typescript/components/TokenHeader/TokenHeader.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import TokenAccount from "./TokenAccount"; -import { TokenMetadata20 } from "../../common/supabase-types"; -import { supabase } from "../../common/supabase-client"; -import styles from "./TokenHeader.module.css"; -import { useEffect, useState } from "react"; - -function TokenHeader(props: any) { - const [tokenData, setTokenData] = useState(); - - const fetchData = async () => { - try { - const { data } = await supabase - .from(`token_metadata_erc20`) - .select("*") - .match({ address: props.token_address }); - console.log(data); - if (data?.[0]) { - setTokenData(data[0]); - } else { - throw new Error("Failed to load token details"); - } - } catch (error: any) { - console.error(error.message); - } - }; - - useEffect(() => { - fetchData(); - }, []); - - return ( -
- -
-

- Address: {props.token_address} -

-

- Created at: {tokenData?.created_at} -

-

- Total supply: - {tokenData?.total_supply} -

-
-
- ); -} - -export default TokenHeader; diff --git a/yarn.lock b/yarn.lock index f80ced508b..0d13664875 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7373,6 +7373,9 @@ __metadata: "@mui/icons-material": "npm:5.15.10" "@mui/material": "npm:5.15.15" "@supabase/supabase-js": "npm:1.35.6" + "@tanstack/eslint-plugin-query": "npm:5.28.11" + "@tanstack/react-query": "npm:5.29.2" + "@tanstack/react-query-devtools": "npm:5.29.2" "@types/react": "npm:18.2.43" "@types/react-dom": "npm:18.2.17" "@types/sort-by": "npm:1" @@ -13889,6 +13892,54 @@ __metadata: languageName: node linkType: hard +"@tanstack/eslint-plugin-query@npm:5.28.11": + version: 5.28.11 + resolution: "@tanstack/eslint-plugin-query@npm:5.28.11" + dependencies: + "@typescript-eslint/utils": "npm:^6.20.0" + peerDependencies: + eslint: ^8.0.0 + checksum: 10/f5fb6fa4dea4917ec173e86cc7fe733abe9ea7942283af34292f90724321a7fad57f4d918326a49fbf4c301c0f4fe749090f673766679e8d57dbaed0c1a53cc4 + languageName: node + linkType: hard + +"@tanstack/query-core@npm:5.29.0": + version: 5.29.0 + resolution: "@tanstack/query-core@npm:5.29.0" + checksum: 10/a58f1f899292daea66bf8341a4e6b8f96064c0a45bbcb350d68b004b871c6d2894dcecb268389695b093167b0a0acb6bcf5d01f52a335ffb128348b6705450b1 + languageName: node + linkType: hard + +"@tanstack/query-devtools@npm:5.28.10": + version: 5.28.10 + resolution: "@tanstack/query-devtools@npm:5.28.10" + checksum: 10/3caa49e83507abc1fb5d44283738d6c5d88fd4b7d6f6ec2c26aa77d938157679fc99da51e1f832295e76940d9d90f793549ae927d11bfba67fcc2dfeaf0d5a43 + languageName: node + linkType: hard + +"@tanstack/react-query-devtools@npm:5.29.2": + version: 5.29.2 + resolution: "@tanstack/react-query-devtools@npm:5.29.2" + dependencies: + "@tanstack/query-devtools": "npm:5.28.10" + peerDependencies: + "@tanstack/react-query": ^5.29.2 + react: ^18.0.0 + checksum: 10/83226e7c5eefa1faddcf1f1ddcf65137f3926468d0e029bbd4bae124606e00ff11641cd9624f3bc2b07159ea6333752c2382359bfab107b98d83ceb1f56bdc30 + languageName: node + linkType: hard + +"@tanstack/react-query@npm:5.29.2": + version: 5.29.2 + resolution: "@tanstack/react-query@npm:5.29.2" + dependencies: + "@tanstack/query-core": "npm:5.29.0" + peerDependencies: + react: ^18.0.0 + checksum: 10/fff25a36d764b0bec7d4ec3f499a74881abf043e81a8f97c71ffffd52fb6b1cfd60f0199ac4150902beccc95e80b7c32ec3a56ae2ee07bdd388e14bb4546123b + languageName: node + linkType: hard + "@testing-library/dom@npm:^8.5.0": version: 8.20.1 resolution: "@testing-library/dom@npm:8.20.1" @@ -16260,6 +16311,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/scope-manager@npm:6.21.0" + dependencies: + "@typescript-eslint/types": "npm:6.21.0" + "@typescript-eslint/visitor-keys": "npm:6.21.0" + checksum: 10/fe91ac52ca8e09356a71dc1a2f2c326480f3cccfec6b2b6d9154c1a90651ab8ea270b07c67df5678956c3bbf0bbe7113ab68f68f21b20912ea528b1214197395 + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:7.1.0": version: 7.1.0 resolution: "@typescript-eslint/scope-manager@npm:7.1.0" @@ -16311,6 +16372,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/types@npm:6.21.0" + checksum: 10/e26da86d6f36ca5b6ef6322619f8ec55aabcd7d43c840c977ae13ae2c964c3091fc92eb33730d8be08927c9de38466c5323e78bfb270a9ff1d3611fe821046c5 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:7.1.0": version: 7.1.0 resolution: "@typescript-eslint/types@npm:7.1.0" @@ -16336,6 +16404,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/typescript-estree@npm:6.21.0" + dependencies: + "@typescript-eslint/types": "npm:6.21.0" + "@typescript-eslint/visitor-keys": "npm:6.21.0" + debug: "npm:^4.3.4" + globby: "npm:^11.1.0" + is-glob: "npm:^4.0.3" + minimatch: "npm:9.0.3" + semver: "npm:^7.5.4" + ts-api-utils: "npm:^1.0.1" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/b32fa35fca2a229e0f5f06793e5359ff9269f63e9705e858df95d55ca2cd7fdb5b3e75b284095a992c48c5fc46a1431a1a4b6747ede2dd08929dc1cbacc589b8 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:7.1.0": version: 7.1.0 resolution: "@typescript-eslint/typescript-estree@npm:7.1.0" @@ -16390,6 +16477,23 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:^6.20.0": + version: 6.21.0 + resolution: "@typescript-eslint/utils@npm:6.21.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@types/json-schema": "npm:^7.0.12" + "@types/semver": "npm:^7.5.0" + "@typescript-eslint/scope-manager": "npm:6.21.0" + "@typescript-eslint/types": "npm:6.21.0" + "@typescript-eslint/typescript-estree": "npm:6.21.0" + semver: "npm:^7.5.4" + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + checksum: 10/b404a2c55a425a79d054346ae123087d30c7ecf7ed7abcf680c47bf70c1de4fabadc63434f3f460b2fa63df76bc9e4a0b9fa2383bb8a9fcd62733fb5c4e4f3e3 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" @@ -16400,6 +16504,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/visitor-keys@npm:6.21.0" + dependencies: + "@typescript-eslint/types": "npm:6.21.0" + eslint-visitor-keys: "npm:^3.4.1" + checksum: 10/30422cdc1e2ffad203df40351a031254b272f9c6f2b7e02e9bfa39e3fc2c7b1c6130333b0057412968deda17a3a68a578a78929a8139c6acef44d9d841dc72e1 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:7.1.0": version: 7.1.0 resolution: "@typescript-eslint/visitor-keys@npm:7.1.0"