From ccf9c61da687b5ef14865eac06c2237af5fccb55 Mon Sep 17 00:00:00 2001 From: Martin Stefcek <35243812+Cifko@users.noreply.github.com> Date: Fri, 29 Sep 2023 12:43:03 +0200 Subject: [PATCH] feat: blocks in vn ui (#671) Description --- Changes in the VN UI - Add `blocks` - Remove `recent transactions` Motivation and Context --- How Has This Been Tested? --- Manually. What process can a PR reviewer use to test or verify this change? --- Run a VN on network with blocks. Breaking Changes --- - [x] None - [ ] Requires data directory to be deleted - [ ] Other - Please specify --- Cargo.lock | 1 + .../src/json_rpc/handlers.rs | 45 +- .../src/json_rpc/server.rs | 9 +- .../tari_validator_node_web_ui/src/App.tsx | 19 +- .../src/Components/MenuItems.tsx | 7 +- .../src/Components/SearchFilter.tsx | 2 +- .../tari_validator_node_web_ui/src/index.tsx | 11 +- .../src/routes/Blocks/BlockDetails.tsx | 249 +++++++++ .../Blocks.tsx} | 10 +- .../src/routes/Blocks/Transactions.tsx | 70 +++ .../src/routes/VN/Components/Blocks.tsx | 482 ++++++++++++++++++ .../VN/Components/RecentTransactions.tsx | 397 --------------- .../src/routes/VN/ValidatorNode.css | 5 + .../src/routes/VN/ValidatorNode.tsx | 14 +- .../src/utils/helpers.tsx | 48 +- .../src/utils/json_rpc.tsx | 74 +-- clients/validator_node_client/src/types.rs | 28 + dan_layer/state_store_sqlite/src/reader.rs | 58 +++ .../src/sql_models/block.rs | 1 + dan_layer/storage/Cargo.toml | 1 + .../storage/src/consensus_models/block.rs | 21 + dan_layer/storage/src/state_store/mod.rs | 10 +- 22 files changed, 1082 insertions(+), 480 deletions(-) create mode 100644 applications/tari_validator_node_web_ui/src/routes/Blocks/BlockDetails.tsx rename applications/tari_validator_node_web_ui/src/routes/{RecentTransactions/RecentTransactions.tsx => Blocks/Blocks.tsx} (88%) create mode 100644 applications/tari_validator_node_web_ui/src/routes/Blocks/Transactions.tsx create mode 100644 applications/tari_validator_node_web_ui/src/routes/VN/Components/Blocks.tsx delete mode 100644 applications/tari_validator_node_web_ui/src/routes/VN/Components/RecentTransactions.tsx diff --git a/Cargo.lock b/Cargo.lock index 7e5d1ff67..1e89ab0b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7686,6 +7686,7 @@ dependencies = [ "tari_transaction", "tari_utilities 0.4.10", "thiserror", + "time 0.3.22", ] [[package]] diff --git a/applications/tari_validator_node/src/json_rpc/handlers.rs b/applications/tari_validator_node/src/json_rpc/handlers.rs index c9523ceab..6a7f82cbf 100644 --- a/applications/tari_validator_node/src/json_rpc/handlers.rs +++ b/applications/tari_validator_node/src/json_rpc/handlers.rs @@ -57,6 +57,9 @@ use tari_validator_node_client::types::{ AddPeerResponse, CommitteeShardInfo, DryRunTransactionFinalizeResult, + GetBlockRequest, + GetBlockResponse, + GetBlocksCountResponse, GetCommitteeRequest, GetEpochManagerStatsResponse, GetIdentityResponse, @@ -168,8 +171,7 @@ impl JsonRpcHandlers { target: LOG_TARGET, "Transaction {} has {} involved shards", transaction.hash(), - transaction - .num_involved_shards() + transaction.num_involved_shards() ); let tx_id = *transaction.id(); @@ -389,6 +391,45 @@ impl JsonRpcHandlers { })) } + pub async fn get_block(&self, value: JsonRpcExtractor) -> JrpcResult { + let answer_id = value.get_answer_id(); + let data: GetBlockRequest = value.parse_params()?; + let mut tx = self.state_store.create_read_tx().unwrap(); + match Block::get(&mut tx, &data.block_id) { + Ok(block) => { + let res = GetBlockResponse { block }; + Ok(JsonRpcResponse::success(answer_id, res)) + }, + Err(e) => Err(JsonRpcResponse::error( + answer_id, + JsonRpcError::new( + JsonRpcErrorReason::InvalidParams, + format!("Something went wrong: {}", e), + json::Value::Null, + ), + )), + } + } + + pub async fn get_blocks_count(&self, value: JsonRpcExtractor) -> JrpcResult { + let answer_id = value.get_answer_id(); + let mut tx = self.state_store.create_read_tx().unwrap(); + match Block::get_count(&mut tx) { + Ok(count) => { + let res = GetBlocksCountResponse { count }; + Ok(JsonRpcResponse::success(answer_id, res)) + }, + Err(e) => Err(JsonRpcResponse::error( + answer_id, + JsonRpcError::new( + JsonRpcErrorReason::InternalError, + format!("Something went wrong: {}", e), + json::Value::Null, + ), + )), + } + } + pub async fn register_validator_node(&self, value: JsonRpcExtractor) -> JrpcResult { let answer_id = value.get_answer_id(); let req: RegisterValidatorNodeRequest = value.parse_params()?; diff --git a/applications/tari_validator_node/src/json_rpc/server.rs b/applications/tari_validator_node/src/json_rpc/server.rs index c44f7f60e..768a64b99 100644 --- a/applications/tari_validator_node/src/json_rpc/server.rs +++ b/applications/tari_validator_node/src/json_rpc/server.rs @@ -68,6 +68,9 @@ async fn handler(Extension(handlers): Extension>, value: Js "get_substates_created_by_transaction" => handlers.get_substates_created_by_transaction(value).await, "get_substates_destroyed_by_transaction" => handlers.get_substates_destroyed_by_transaction(value).await, "list_blocks" => handlers.list_blocks(value).await, + // Blocks + "get_block" => handlers.get_block(value).await, + "get_blocks_count" => handlers.get_blocks_count(value).await, // Template "get_template" => handlers.get_template(value).await, "get_templates" => handlers.get_templates(value).await, @@ -94,7 +97,11 @@ async fn handler(Extension(handlers): Extension>, value: Js if let Err(ref e) = result { match &e.result { JsonRpcAnswer::Result(val) => { - error!(target: LOG_TARGET, "🚨 JSON-RPC request failed: {}", serde_json::to_string_pretty(val).unwrap_or_else(|e| e.to_string())); + error!( + target: LOG_TARGET, + "🚨 JSON-RPC request failed: {}", + serde_json::to_string_pretty(val).unwrap_or_else(|e| e.to_string()) + ); }, JsonRpcAnswer::Error(err) => { error!(target: LOG_TARGET, "🚨 JSON-RPC request failed: {}", err); diff --git a/applications/tari_validator_node_web_ui/src/App.tsx b/applications/tari_validator_node_web_ui/src/App.tsx index 9f1d42032..aa98649a7 100644 --- a/applications/tari_validator_node_web_ui/src/App.tsx +++ b/applications/tari_validator_node_web_ui/src/App.tsx @@ -26,7 +26,7 @@ import Committees from "./routes/Committees/CommitteesLayout"; import ValidatorNode from "./routes/VN/ValidatorNode"; import Connections from "./routes/Connections/Connections"; import Fees from "./routes/Fees/Fees"; -import RecentTransactions from "./routes/RecentTransactions/RecentTransactions"; +import Blocks from "./routes/Blocks/Blocks"; import Templates from "./routes/Templates/Templates"; import ValidatorNodes from "./routes/ValidatorNodes/ValidatorNodes"; import ErrorPage from "./routes/ErrorPage"; @@ -42,6 +42,7 @@ import { getShardKey, } from "./utils/json_rpc"; import TransactionDetails from "./routes/Transactions/TransactionDetails"; +import BlockDetails from "./routes/Blocks/BlockDetails"; interface IContext { epoch: IEpoch | undefined; @@ -83,6 +84,16 @@ export const breadcrumbRoutes = [ path: "/transactions", dynamic: false, }, + { + label: "Blocks", + path: "/blocks", + dynamic: false, + }, + { + label: "Blocks", + path: "/blocks/:blockId", + dynamic: true, + }, { label: "Templates", path: "/templates", @@ -168,9 +179,6 @@ export default function App() { ); } }, [epoch, identity]); - useEffect(() => { - getRecentTransactions(); - }, []); return ( <> @@ -181,7 +189,7 @@ export default function App() { } /> } /> } /> - } /> + } /> } /> } /> } /> @@ -189,6 +197,7 @@ export default function App() { path="transactions/:transactionHash" element={} /> + } /> } /> } /> } /> diff --git a/applications/tari_validator_node_web_ui/src/Components/MenuItems.tsx b/applications/tari_validator_node_web_ui/src/Components/MenuItems.tsx index d9a38cf8e..e38567bd5 100644 --- a/applications/tari_validator_node_web_ui/src/Components/MenuItems.tsx +++ b/applications/tari_validator_node_web_ui/src/Components/MenuItems.tsx @@ -41,6 +41,7 @@ import { IoLayers, IoCodeDownloadOutline, IoCodeDownload, + IoBarChartSharp, } from 'react-icons/io5'; import Tooltip from '@mui/material/Tooltip'; import Fade from '@mui/material/Fade'; @@ -65,10 +66,10 @@ const mainItems = [ link: '/', }, { - title: 'Recent Transactions', - icon: , + title: 'Blocks', + icon: , activeIcon: , - link: 'transactions', + link: 'blocks', }, { title: 'Committees', diff --git a/applications/tari_validator_node_web_ui/src/Components/SearchFilter.tsx b/applications/tari_validator_node_web_ui/src/Components/SearchFilter.tsx index 2c1e6b9a6..aa97eca4d 100644 --- a/applications/tari_validator_node_web_ui/src/Components/SearchFilter.tsx +++ b/applications/tari_validator_node_web_ui/src/Components/SearchFilter.tsx @@ -94,7 +94,7 @@ const TransactionFilter: React.FC = ({ // original array, and set its "show" property to true filteredRows.forEach((filteredRow: any) => { const index = updatedObject.findIndex( - (item : any) => item.transaction_hash === filteredRow.transaction_hash + (item : any) => item.id === filteredRow.id ); if (index !== -1) { updatedObject[index].show = true; diff --git a/applications/tari_validator_node_web_ui/src/index.tsx b/applications/tari_validator_node_web_ui/src/index.tsx index 27e02864d..d77adfb5d 100644 --- a/applications/tari_validator_node_web_ui/src/index.tsx +++ b/applications/tari_validator_node_web_ui/src/index.tsx @@ -30,13 +30,14 @@ import Committees from './routes/Committees/CommitteesLayout'; import Connections from './routes/Connections/Connections'; import Fees from './routes/Fees/Fees'; import Mempool from './routes/Mempool/Mempool'; -import RecentTransactions from './routes/RecentTransactions/RecentTransactions'; +import Blocks from './routes/Blocks/Blocks'; import Templates from './routes/Templates/Templates'; import ValidatorNodes from './routes/ValidatorNodes/ValidatorNodes'; import ErrorPage from './routes/ErrorPage'; import TemplateFunctions from './routes/VN/Components/TemplateFunctions'; import CommitteeMembers from './routes/Committees/CommitteeMembers'; import TransactionDetails from './routes/Transactions/TransactionDetails'; +import BlockDetails from './routes/Blocks/BlockDetails'; const router = createBrowserRouter([ { @@ -53,8 +54,12 @@ const router = createBrowserRouter([ element: , }, { - path: 'transactions', - element: , + path: 'blocks', + element: , + }, + { + path: 'blocks/:blockId', + element: , }, { path: 'templates', diff --git a/applications/tari_validator_node_web_ui/src/routes/Blocks/BlockDetails.tsx b/applications/tari_validator_node_web_ui/src/routes/Blocks/BlockDetails.tsx new file mode 100644 index 000000000..184b9f2a5 --- /dev/null +++ b/applications/tari_validator_node_web_ui/src/routes/Blocks/BlockDetails.tsx @@ -0,0 +1,249 @@ +// Copyright 2022. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; +// import { transactionsGet } from '../../utils/json_rpc'; +import { Accordion, AccordionDetails, AccordionSummary } from "../../Components/Accordion"; +import { Grid, Table, TableContainer, TableBody, TableRow, TableCell, Button, Fade, Alert } from "@mui/material"; +import Typography from "@mui/material/Typography"; +import { DataTableCell, StyledPaper } from "../../Components/StyledComponents"; +import PageHeading from "../../Components/PageHeading"; +import StatusChip from "../../Components/StatusChip"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import Loading from "../../Components/Loading"; +import { + getBlock, + getIdentity, +} from "../../utils/json_rpc"; +import Transactions from "./Transactions"; +import { primitiveDateTimeToDate, primitiveDateTimeToSecs } from "../../utils/helpers"; + +export default function BlockDetails() { + const { blockId } = useParams(); + const [expandedPanels, setExpandedPanels] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(); + const [block, setBlock] = useState(); + const [prepare, setPrepare] = useState([]); + const [localPrepared, setLocalPrepared] = useState([]); + const [accept, setAccept] = useState([]); + const [identity, setIdentity] = useState(); + const [blockTime, setBlockTime] = useState(0); + + useEffect(() => { + if (blockId !== undefined) { + Promise.all([getBlock(blockId), getIdentity()]) + .then(([resp, identity]) => { + setIdentity(identity); + setBlock(resp.block); + if (resp?.block?.justify?.block_id) { + getBlock(resp.block.justify.block_id).then((justify) => { + let blockTime = primitiveDateTimeToSecs(resp.block.stored_at); + let justifyTime = primitiveDateTimeToSecs(justify.block.stored_at); + setBlockTime(blockTime - justifyTime); + }); + } + setPrepare([]); + setLocalPrepared([]); + setAccept([]); + for (let command of resp.block.commands) { + if (command.Prepare) { + setPrepare((prepare) => [...prepare, command.Prepare]); + } else if (command.LocalPrepared) { + setLocalPrepared((localPrepared) => [...localPrepared, command.LocalPrepared]); + } else if (command.Accept) { + setAccept((accept) => [...accept, command.Accept]); + } + } + }) + .catch((err) => { + setError(err && err.message ? err.message : `Unknown error: ${JSON.stringify(err)}`); + }) + .finally(() => { + setLoading(false); + }); + } + }, [blockId]); + + const handleChange = (panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => { + setExpandedPanels((prevExpandedPanels) => { + if (isExpanded) { + return [...prevExpandedPanels, panel]; + } else { + return prevExpandedPanels.filter((p) => p !== panel); + } + }); + }; + + const expandAll = () => { + setExpandedPanels(["panel1", "panel2", "panel3", "panel4", "panel5"]); + }; + + const collapseAll = () => { + setExpandedPanels([]); + }; + return ( + <> + + Block Details + + + + {loading ? ( + + ) : ( + +
+ {error ? ( + {error} + ) : ( + <> + + + + + Block ID + {blockId} + + + Epoch + {block.epoch} + + + Height + {block.height} + + + Parent block + + {block.parent} + + + + Total Fees + +
+ {block.total_leader_fee} +
+
+
+ + Status + + + + + + Proposed by + {block.proposed_by} + + + Block time + {blockTime} secs + + + Stored at + {primitiveDateTimeToDate(block.stored_at).toLocaleString()} + +
+
+
+
+ More Info +
+ + +
+
+ + )} + {prepare.length > 0 && ( + + + Prepare + + + + + + )} + {localPrepared.length > 0 && ( + + + Local prepared + + + + + + )} + {accept.length > 0 && ( + + + Accept + + + + + + )} +
+
+ )} +
+
+ + ); +} diff --git a/applications/tari_validator_node_web_ui/src/routes/RecentTransactions/RecentTransactions.tsx b/applications/tari_validator_node_web_ui/src/routes/Blocks/Blocks.tsx similarity index 88% rename from applications/tari_validator_node_web_ui/src/routes/RecentTransactions/RecentTransactions.tsx rename to applications/tari_validator_node_web_ui/src/routes/Blocks/Blocks.tsx index 55c9e8ae3..afb7b2cfa 100644 --- a/applications/tari_validator_node_web_ui/src/routes/RecentTransactions/RecentTransactions.tsx +++ b/applications/tari_validator_node_web_ui/src/routes/Blocks/Blocks.tsx @@ -23,21 +23,21 @@ import PageHeading from '../../Components/PageHeading'; import Grid from '@mui/material/Grid'; import { StyledPaper } from '../../Components/StyledComponents'; -import RecentTransactions from '../VN/Components/RecentTransactions'; +import Blocks from '../VN/Components/Blocks'; -function RecentTransactionsLayout() { +function BlocksLayout() { return ( <> - Recent Transactions + Blocks - + ); } -export default RecentTransactionsLayout; +export default BlocksLayout; diff --git a/applications/tari_validator_node_web_ui/src/routes/Blocks/Transactions.tsx b/applications/tari_validator_node_web_ui/src/routes/Blocks/Transactions.tsx new file mode 100644 index 000000000..5090b8d74 --- /dev/null +++ b/applications/tari_validator_node_web_ui/src/routes/Blocks/Transactions.tsx @@ -0,0 +1,70 @@ +// Copyright 2022. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import React from "react"; +import { + Grid, + Table, + TableContainer, + TableBody, + TableHead, + TableRow, + TableCell, +} from "@mui/material"; +import StatusChip from "../../Components/StatusChip"; + +function Transaction({ transaction }: any) { + return ( + + + {transaction.id} + + + + + {transaction.leader_fee} + {transaction.transaction_fee} + + ); +} + +export default function Transactions({ transactions }: any) { + return ( + + + + + Transaction ID + Decision + Leader fee + Transaction fee + + + + {transactions.map((tx: any) => ( + + ))} + +
+
+ ); +} diff --git a/applications/tari_validator_node_web_ui/src/routes/VN/Components/Blocks.tsx b/applications/tari_validator_node_web_ui/src/routes/VN/Components/Blocks.tsx new file mode 100644 index 000000000..761c65a8e --- /dev/null +++ b/applications/tari_validator_node_web_ui/src/routes/VN/Components/Blocks.tsx @@ -0,0 +1,482 @@ +// Copyright 2022. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import { useEffect, useState } from "react"; +import { + listBlocks, + getBlocksCount, + getIdentity, + getRecentTransactions, + getTransaction, + getUpSubstates, +} from "../../../utils/json_rpc"; +import { toHexString } from "./helpers"; +import { Link } from "react-router-dom"; +import { primitiveDateTimeToDate, primitiveDateTimeToSecs, renderJson } from "../../../utils/helpers"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import { DataTableCell, CodeBlock, AccordionIconButton, BoxHeading2 } from "../../../Components/StyledComponents"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import Collapse from "@mui/material/Collapse"; +import TablePagination from "@mui/material/TablePagination"; +import Typography from "@mui/material/Typography"; +import HeadingMenu from "../../../Components/HeadingMenu"; +import SearchFilter from "../../../Components/SearchFilter"; +import Fade from "@mui/material/Fade"; +import StatusChip from "../../../Components/StatusChip"; + +// TODO: fill this, and change instructions in IRecentTransaction +// interface IInstruction { +// } + +interface ISignature { + public_nonce: string; + signature: string; +} +interface ITransactionSignature { + public_key: string; + signature: ISignature; +} + +interface IRecentTransaction { + id: string; + fee_instructions: any[]; + instructions: any[]; + signature: ITransactionSignature; + inputs: string[]; + input_refs: string[]; + outputs: string[]; + filled_inputs: string[]; + filled_outputs: string[]; +} + +// TODO: fill these () +type IBlockId = string; + +export interface IQuorumCertificate { + block_id: IBlockId, + decision: string; +} + +type INodeHeight = number; + +type IEpoch = number; + +type IPublicKey = string; + +type IFixedHash = string; + +export interface ICommand {} + +export interface IBlock { + id: IBlockId; + parent: IBlockId; + justify: IQuorumCertificate; + height: INodeHeight; + epoch: IEpoch; + proposed_by: IPublicKey; + total_leader_fee: number; + merkle_root: IFixedHash; + stored_at: number[], + commands: ICommand[]; +} + +export interface ITableBlock { + id: string; + epoch: number; + height: number; + decision: string; + total_leader_fee: number; + proposed_by_me: boolean; + proposed_by:string; + transactions_cnt: number; + block_time: number; + stored_at: Date; + show?: boolean; +} + +interface IGetBlockReponse { + blocks: IBlock[]; +} + +type ColumnKey = keyof ITableBlock; + +function Blocks() { + const [blocks, setBlocks] = useState([]); + const [lastSort, setLastSort] = useState({ column: "", order: -1 }); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [blockCount, setBlockCount] = useState(0); + + // Avoid a layout jump when reaching the last page with empty rows. + const emptyRows = page > 0 ? Math.max(0, (1 + page) * rowsPerPage - blocks.length) : 0; + + const handleChangePage = (event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + useEffect(() => { + Promise.all([getIdentity(), getBlocksCount()]).then(([identity, resp]) => { + // TODO: remove this once the pagination is done + // resp.count = 100; + setBlockCount(resp.count); + listBlocks(null, resp.count).then((resp: IGetBlockReponse) => { + let times = Object.fromEntries(resp.blocks.map((block:IBlock) => [block.id, primitiveDateTimeToSecs(block.stored_at)])); + setBlocks( + resp.blocks.map((block: IBlock) => { + return { + id: block.id, + epoch: block.epoch, + height: block.height, + decision: block.justify.decision, + total_leader_fee: block.total_leader_fee, + proposed_by_me: block.proposed_by == identity.public_key, + transactions_cnt: block.commands.length, + block_time: times[block.id] - times[block.justify.block_id], + stored_at: primitiveDateTimeToDate(block.stored_at), + proposed_by: block.proposed_by, + show: true, + }; + }) + ); + }); + }); + }, []); + const sort = (column: ColumnKey, order: number) => { + if (column) { + setBlocks( + [...blocks].sort((r0: any, r1: any) => (r0[column] > r1[column] ? order : r0[column] < r1[column] ? -order : 0)) + ); + setLastSort({ column, order }); + } + }; + return ( + <> + + row.id.toLowerCase().includes(value.toLowerCase()), + }, + { + title: "Epoch", + value: "epoch", + filterFn: (value: string, row: ITableBlock) => String(row.epoch).includes(value), + }, + { + title: "Height", + value: "height", + filterFn: (value: string, row: ITableBlock) => String(row.height).includes(value), + }, + { + title: "Decision", + value: "decision", + filterFn: (value: string, row: ITableBlock) => row.decision.includes(value), + }, + { + title: "# of Transactions", + value: "transactions_cnt", + filterFn: (value: string, row: ITableBlock) => String(row.transactions_cnt).includes(value), + }, + { + title: "Total fees", + value: "total_leader_fee", + filterFn: (value: string, row: ITableBlock) => String(row.total_leader_fee).includes(value), + }, + ]} + placeholder="Search for Transactions" + defaultSearch="block_id" + /> + + + + + + + sort("id", 1), + icon: , + }, + { + title: "Sort Descending", + fn: () => sort("id", -1), + icon: , + }, + ]} + showArrow + lastSort={lastSort} + columnName="id" + sortFunction={sort} + /> + + + sort("epoch", 1), + icon: , + }, + { + title: "Sort Descending", + fn: () => sort("epoch", -1), + icon: , + }, + ]} + showArrow + lastSort={lastSort} + columnName="epoch" + sortFunction={sort} + /> + + + sort("height", 1), + icon: , + }, + { + title: "Sort Descending", + fn: () => sort("height", -1), + icon: , + }, + ]} + showArrow + lastSort={lastSort} + columnName="height" + sortFunction={sort} + /> + + + sort("decision", 1), + icon: , + }, + { + title: "Sort Descending", + fn: () => sort("decision", -1), + icon: , + }, + ]} + showArrow + lastSort={lastSort} + columnName="decision" + sortFunction={sort} + /> + + + sort("transactions_cnt", 1), + icon: , + }, + { + title: "Sort Descending", + fn: () => sort("transactions_cnt", -1), + icon: , + }, + ]} + showArrow + lastSort={lastSort} + columnName="transactions_cnt" + sortFunction={sort} + /> + + + sort("total_leader_fee", 1), + icon: , + }, + { + title: "Sort Descending", + fn: () => sort("total_leader_fee", -1), + icon: , + }, + ]} + showArrow + lastSort={lastSort} + columnName="total_leader_fee" + sortFunction={sort} + /> + + + sort("block_time", 1), + icon: , + }, + { + title: "Sort Descending", + fn: () => sort("block_time", -1), + icon: , + }, + ]} + showArrow + lastSort={lastSort} + columnName="block_time" + sortFunction={sort} + /> + + + sort("stored_at", 1), + icon: , + }, + { + title: "Sort Descending", + fn: () => sort("stored_at", -1), + icon: , + }, + ]} + showArrow + lastSort={lastSort} + columnName="stored_at" + sortFunction={sort} + /> + + + sort("proposed_by", 1), + icon: , + }, + { + title: "Sort Descending", + fn: () => sort("proposed_by", -1), + icon: , + }, + ]} + showArrow + lastSort={lastSort} + columnName="proposed_by" + sortFunction={sort} + /> + + + + + {blocks + .filter(({ show }) => show === true) + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map(({ id, epoch, height, decision, total_leader_fee, transactions_cnt, proposed_by_me,stored_at, block_time,proposed_by }) => { + return ( + + + + {id.slice(0,8)} + + + {epoch} + {height} + + + + {transactions_cnt} + +
{total_leader_fee}
+
+ {block_time} secs + {stored_at.toLocaleString()} +
{proposed_by.slice(0,8)}
+
+ ); + })} + {blocks.filter(({ show }) => show === true).length === 0 && ( + + + show === true).length === 0} timeout={500}> + No results found + + + + )} + {emptyRows > 0 && ( + + + + )} +
+
+ transaction.show === true).length} + rowsPerPage={rowsPerPage} + page={page} + onPageChange={handleChangePage} + onRowsPerPageChange={handleChangeRowsPerPage} + /> +
+ + ); +} + +export default Blocks; diff --git a/applications/tari_validator_node_web_ui/src/routes/VN/Components/RecentTransactions.tsx b/applications/tari_validator_node_web_ui/src/routes/VN/Components/RecentTransactions.tsx deleted file mode 100644 index 6380f89b6..000000000 --- a/applications/tari_validator_node_web_ui/src/routes/VN/Components/RecentTransactions.tsx +++ /dev/null @@ -1,397 +0,0 @@ -// Copyright 2022. The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import { useEffect, useState } from "react"; -import { getRecentTransactions, getTransaction, getUpSubstates } from "../../../utils/json_rpc"; -import { toHexString } from "./helpers"; -import { Link } from "react-router-dom"; -import { renderJson } from "../../../utils/helpers"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; -import { DataTableCell, CodeBlock, AccordionIconButton, BoxHeading2 } from "../../../Components/StyledComponents"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; -import Collapse from "@mui/material/Collapse"; -import TablePagination from "@mui/material/TablePagination"; -import Typography from "@mui/material/Typography"; -import HeadingMenu from "../../../Components/HeadingMenu"; -import SearchFilter from "../../../Components/SearchFilter"; -import Fade from "@mui/material/Fade"; -import StatusChip from "../../../Components/StatusChip"; - -// TODO: fill this, and change instructions in IRecentTransaction -// interface IInstruction { -// } - -interface ISignature { - public_nonce: string; - signature: string; -} -interface ITransactionSignature { - public_key: string; - signature: ISignature; -} - -interface IRecentTransaction { - id: string; - fee_instructions: any[]; - instructions: any[]; - signature: ITransactionSignature; - inputs: string[]; - input_refs: string[]; - outputs: string[]; - filled_inputs: string[]; - filled_outputs: string[]; -} - -export interface ITableRecentTransaction { - transaction_hash: string; - status: any; - total_fees_charged: number; - show?: boolean; -} - -type ColumnKey = keyof ITableRecentTransaction; - -// function RowData({ -// id, -// payload_id, -// timestamp, -// instructions, -// meta, -// }: ITableRecentTransaction) { -// const [open1, setOpen1] = useState(false); -// const [open2, setOpen2] = useState(false); - -// return ( -// <> -// -// -// -// {payload_id} -// -// -// -// {timestamp.replace('T', ' ')} -// -// -// { -// setOpen1(!open1); -// setOpen2(false); -// }} -// > -// {open1 ? : } -// -// -// -// { -// setOpen2(!open2); -// setOpen1(false); -// }} -// > -// {open2 ? : } -// -// -// -// -// -// -// -// {renderJson(JSON.parse(meta))} -// -// -// -// -// -// -// -// -// {renderJson(JSON.parse(instructions))} -// -// -// -// -// -// ); -// } - -function RecentTransactions() { - const [recentTransactions, setRecentTransactions] = useState([]); - const [lastSort, setLastSort] = useState({ column: "", order: -1 }); - - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); - - // Avoid a layout jump when reaching the last page with empty rows. - const emptyRows = page > 0 ? Math.max(0, (1 + page) * rowsPerPage - recentTransactions.length) : 0; - - const handleChangePage = (event: unknown, newPage: number) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event: React.ChangeEvent) => { - setRowsPerPage(parseInt(event.target.value, 10)); - setPage(0); - }; - - useEffect(() => { - getRecentTransactions().then((resp) => { - console.log("resp", resp); - setRecentTransactions( - // Display from newest to oldest by reversing - resp.transactions - .slice() - .reverse() - .map( - ({ - id, - fee_instructions, - instructions, - signature, - inputs, - input_refs, - outputs, - filled_inputs, - filled_outputs, - }: IRecentTransaction) => ({ - transaction_hash: id, - total_fees_charged: null, - status: "Loading", - show: true, - }) - ) - ); - for (let tx in resp.transactions) { - Promise.all([getTransaction(resp.transactions[tx].id), getUpSubstates(String(resp.transactions[tx].id))]).then(([transaction, substates]) => { - setRecentTransactions((prevState: any) => - prevState.map((item: any, index: any) => { - if (tx == index) { - return { - ...item, - status: transaction["transaction"]["final_decision"], - total_fees_charged: substates["substates"].reduce((acc:number,cur:any) => acc+Number(cur?.substate_value?.TransactionReceipt?.fee_receipt?.fee_resource?.Confidential?.revealed_amount || 0), 0), - }; - } - return item; - }) - ); - }); - } - }); - }, []); - const sort = (column: ColumnKey, order: number) => { - // let order = 1; - // if (lastSort.column === column) { - // order = -lastSort.order; - // } - if (column) { - setRecentTransactions( - [...recentTransactions].sort((r0: any, r1: any) => - r0[column] > r1[column] ? order : r0[column] < r1[column] ? -order : 0 - ) - ); - setLastSort({ column, order }); - } - }; - return ( - <> - - { - return row.transaction_hash.toLowerCase().includes(value.toLowerCase()) - }, - }, - { - title: "Status", - value: "status", - filterFn: (value: string, row: ITableRecentTransaction) => row.status.includes(value), - }, - { - title: "Total fees", - value: "total_fees_charged", - filterFn: (value: string, row: ITableRecentTransaction) => String(row.total_fees_charged) == value, - }, - ]} - placeholder="Search for Transactions" - defaultSearch="transaction_hash" - /> - - - - - - - sort("transaction_hash", 1), - icon: , - }, - { - title: "Sort Descending", - fn: () => sort("transaction_hash", -1), - icon: , - }, - ]} - showArrow - lastSort={lastSort} - columnName="transaction_hash" - sortFunction={sort} - /> - - - sort("status", 1), - icon: , - }, - { - title: "Sort Descending", - fn: () => sort("status", -1), - icon: , - }, - ]} - showArrow - lastSort={lastSort} - columnName="status" - sortFunction={sort} - /> - - - sort("total_fees_charged", 1), - icon: , - }, - { - title: "Sort Descending", - fn: () => sort("total_fees_charged", -1), - icon: , - }, - ]} - showArrow - lastSort={lastSort} - columnName="total_fees_charged" - sortFunction={sort} - /> - - - - - {recentTransactions - .filter(({ show }) => show === true) - .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map(({ transaction_hash, status, total_fees_charged }) => { - return ( - - - - {transaction_hash} - - - - - - {total_fees_charged} - - ); - })} - {recentTransactions.filter(({ show }) => show === true).length === 0 && ( - - - show === true).length === 0} timeout={500}> - No results found - - - - )} - {emptyRows > 0 && ( - - - - )} - -
- transaction.show === true).length} - rowsPerPage={rowsPerPage} - page={page} - onPageChange={handleChangePage} - onRowsPerPageChange={handleChangeRowsPerPage} - /> -
- - ); -} - -export default RecentTransactions; diff --git a/applications/tari_validator_node_web_ui/src/routes/VN/ValidatorNode.css b/applications/tari_validator_node_web_ui/src/routes/VN/ValidatorNode.css index 17dac421b..51499f97d 100644 --- a/applications/tari_validator_node_web_ui/src/routes/VN/ValidatorNode.css +++ b/applications/tari_validator_node_web_ui/src/routes/VN/ValidatorNode.css @@ -167,3 +167,8 @@ ul { .other { color: red; } + +.my_money { + color:green; + font-weight: bolder; +} diff --git a/applications/tari_validator_node_web_ui/src/routes/VN/ValidatorNode.tsx b/applications/tari_validator_node_web_ui/src/routes/VN/ValidatorNode.tsx index 50812bc8b..7c0f3bb87 100644 --- a/applications/tari_validator_node_web_ui/src/routes/VN/ValidatorNode.tsx +++ b/applications/tari_validator_node_web_ui/src/routes/VN/ValidatorNode.tsx @@ -27,7 +27,7 @@ import Connections from "./Components/Connections"; import Fees from "./Components/Fees"; import Info from "./Components/Info"; import Mempool from "./Components/Mempool"; -import RecentTransactions from "./Components/RecentTransactions"; +import Blocks from "./Components/Blocks"; import Templates from "./Components/Templates"; import "./ValidatorNode.css"; import { StyledPaper } from "../../Components/StyledComponents"; @@ -38,8 +38,7 @@ import { GetNetworkCommitteesResponse } from "../../utils/interfaces"; import { getNetworkCommittees } from "../../utils/json_rpc"; function ValidatorNode() { - const [committees, setCommittees] = - useState(null); + const [committees, setCommittees] = useState(null); const { epoch, identity, shardKey, error } = useContext(VNContext); @@ -69,10 +68,7 @@ function ValidatorNode() { {committees ? ( <> - + ) : null} @@ -102,11 +98,11 @@ function ValidatorNode() { - Recent Transactions + Blocks - + diff --git a/applications/tari_validator_node_web_ui/src/utils/helpers.tsx b/applications/tari_validator_node_web_ui/src/utils/helpers.tsx index 15cf704ef..d34ec5182 100644 --- a/applications/tari_validator_node_web_ui/src/utils/helpers.tsx +++ b/applications/tari_validator_node_web_ui/src/utils/helpers.tsx @@ -20,8 +20,8 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import { ChangeEvent } from 'react'; -import { toHexString } from '../routes/VN/Components/helpers'; +import { ChangeEvent } from "react"; +import { toHexString } from "../routes/VN/Components/helpers"; const renderJson = (json: any) => { if (!json) { @@ -43,10 +43,10 @@ const renderJson = (json: any) => { ], ); - } else if (typeof json === 'object') { + } else if (typeof json === "object") { return ( <> - {'{'} + {"{"}
    {Object.keys(json).map((key) => (
  • @@ -54,22 +54,21 @@ const renderJson = (json: any) => {
  • ))}
- {'}'} + {"}"} ); } else { - if (typeof json === 'string') - return "{json}"; + if (typeof json === "string") return "{json}"; return {json}; } }; function removeTagged(obj: any) { if (obj === undefined) { - return 'undefined'; + return "undefined"; } - if (obj['@@TAGGED@@'] !== undefined) { - return obj['@@TAGGED@@'][1]; + if (obj["@@TAGGED@@"] !== undefined) { + return obj["@@TAGGED@@"][1]; } return obj; } @@ -77,24 +76,20 @@ function removeTagged(obj: any) { function fromHexString(hexString: string) { let res = []; for (let i = 0; i < hexString.length; i += 2) { - res.push(Number('0x' + hexString.substring(i, i + 2))); + res.push(Number("0x" + hexString.substring(i, i + 2))); } return res; } function shortenString(string: string, start: number = 8, end: number = 8) { - return string.substring(0, start) + '...' + string.slice(-end); + return string.substring(0, start) + "..." + string.slice(-end); } function emptyRows(page: number, rowsPerPage: number, array: any[]) { return page > 0 ? Math.max(0, (1 + page) * rowsPerPage - array.length) : 0; } -function handleChangePage( - event: unknown, - newPage: number, - setPage: React.Dispatch> -) { +function handleChangePage(event: unknown, newPage: number, setPage: React.Dispatch>) { setPage(newPage); } @@ -107,12 +102,23 @@ function handleChangeRowsPerPage( setPage(0); } +function primitiveDateTimeToDate([year, dayOfTheYear, hour, minute, second, nanos]: number[]): Date { + return new Date(year, 0, dayOfTheYear, hour, minute, second, nanos / 1000000); +} + +function primitiveDateTimeToSecs([year, dayOfTheYear, hour, minute, second, nanos]: number[]): number { + // The datetime is in format [year, day of the year, hour, minute, second, nanos] + return new Date(year, 0, dayOfTheYear, hour, minute, second, nanos / 1000000).valueOf() / 1000; +} + export { - renderJson, - fromHexString, - shortenString, - removeTagged, emptyRows, + fromHexString, handleChangePage, handleChangeRowsPerPage, + primitiveDateTimeToDate, + primitiveDateTimeToSecs, + removeTagged, + renderJson, + shortenString, }; diff --git a/applications/tari_validator_node_web_ui/src/utils/json_rpc.tsx b/applications/tari_validator_node_web_ui/src/utils/json_rpc.tsx index ad3f4597f..f41322374 100644 --- a/applications/tari_validator_node_web_ui/src/utils/json_rpc.tsx +++ b/applications/tari_validator_node_web_ui/src/utils/json_rpc.tsx @@ -20,66 +20,68 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import { GetNetworkCommitteesResponse } from './interfaces'; +import { GetNetworkCommitteesResponse } from "./interfaces"; async function jsonRpc(method: string, params: any = null) { let id = 0; id += 1; - let address = '127.0.0.1:18010'; + let address = "127.0.0.1:18010"; try { - let text = await (await fetch('/json_rpc_address')).text(); + let text = await (await fetch("/json_rpc_address")).text(); if (/^\d+(\.\d+){3}:[0-9]+$/.test(text)) { console.log(`Setting JSON RPC address to ${text}`); address = text; } } catch {} + console.log(method, params); let response = await fetch(`http://${address}`, { - method: 'POST', + method: "POST", body: JSON.stringify({ method: method, - jsonrpc: '2.0', + jsonrpc: "2.0", id: id, params: params, }), headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, }); let json = await response.json(); + console.log(method, json); if (json.error) { throw json.error; } return json.result; } async function getIdentity() { - return await jsonRpc('get_identity'); + return await jsonRpc("get_identity"); } async function getEpochManagerStats() { - return await jsonRpc('get_epoch_manager_stats'); + return await jsonRpc("get_epoch_manager_stats"); } async function getCommsStats() { - return await jsonRpc('get_comms_stats'); + return await jsonRpc("get_comms_stats"); } async function getMempoolStats() { - return await jsonRpc('get_mempool_stats'); + return await jsonRpc("get_mempool_stats"); } async function getShardKey(height: number, public_key: string) { - return await jsonRpc('get_shard_key', [height, public_key]); + return await jsonRpc("get_shard_key", [height, public_key]); } async function getCommittee(epoch: number, shard_id: string) { - return await jsonRpc('get_committee', {epoch, shard_id}); + return await jsonRpc("get_committee", { epoch, shard_id }); } async function getAllVns(epoch: number) { - return await jsonRpc('get_all_vns', epoch); + return await jsonRpc("get_all_vns", epoch); } -async function getNetworkCommittees() : Promise { - return await jsonRpc('get_network_committees', {}); +async function getNetworkCommittees(): Promise { + return await jsonRpc("get_network_committees", {}); } async function getConnections() { - return await jsonRpc('get_connections'); + return await jsonRpc("get_connections"); } async function addPeer(public_key: string, addresses: string[]) { - return await jsonRpc('add_peer', { + return await jsonRpc("add_peer", { public_key, addresses, wait_for_dial: false, @@ -89,36 +91,44 @@ async function registerValidatorNode(feeClaimPublicKeyHex: string) { return await jsonRpc('register_validator_node', { fee_claim_public_key: feeClaimPublicKeyHex }); } async function getRecentTransactions() { - return await jsonRpc('get_recent_transactions'); + return await jsonRpc("get_recent_transactions"); } async function getTransaction(payload_id: string) { - return await jsonRpc('get_transaction', [payload_id]); + return await jsonRpc("get_transaction", [payload_id]); } -async function getFees(start_epoch: number, end_epoch:number, claim_leader_public_key: string) { - return await jsonRpc('get_fees', [ - [start_epoch,end_epoch], - claim_leader_public_key, - ]); +async function getFees(start_epoch: number, end_epoch: number, claim_leader_public_key: string) { + return await jsonRpc("get_fees", [[start_epoch, end_epoch], claim_leader_public_key]); } async function getUpSubstates(payload_id: string) { - return await jsonRpc('get_substates_created_by_transaction', [ - payload_id, - ]); + return await jsonRpc("get_substates_created_by_transaction", [payload_id]); } async function getDownSubstates(payload_id: string) { - return await jsonRpc('get_substates_destroyed_by_transaction', [ - payload_id, - ]); + return await jsonRpc("get_substates_destroyed_by_transaction", [payload_id]); } async function getTemplates(limit: number) { - return await jsonRpc('get_templates', [limit]); + return await jsonRpc("get_templates", [limit]); } async function getTemplate(address: string) { - return await jsonRpc('get_template', [address]); + return await jsonRpc("get_template", [address]); +} + +async function listBlocks(block_id: string | null, limit: number) { + return await jsonRpc("list_blocks", [block_id, limit]); +} + +async function getBlock(block_id: string) { + return await jsonRpc("get_block", [block_id]); +} + +async function getBlocksCount() { + return await jsonRpc("get_blocks_count"); } export { getAllVns, + getBlock, + listBlocks, + getBlocksCount, getCommittee, getCommsStats, getConnections, diff --git a/clients/validator_node_client/src/types.rs b/clients/validator_node_client/src/types.rs index a9ae771e5..3a50427b9 100644 --- a/clients/validator_node_client/src/types.rs +++ b/clients/validator_node_client/src/types.rs @@ -29,6 +29,7 @@ use tari_dan_common_types::{committee::CommitteeShard, shard_bucket::ShardBucket use tari_dan_storage::{ consensus_models::{Block, BlockId, ExecutedTransaction, QuorumDecision, SubstateRecord}, global::models::ValidatorNode, + Ordering, }; use tari_engine_types::{ commit_result::{ExecuteResult, FinalizeResult, RejectReason}, @@ -193,6 +194,21 @@ pub struct ListBlocksResponse { pub blocks: Vec>, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetBlockResponse { + pub block: Block, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetBlocksResponse { + pub blocks: Vec>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetBlocksCountResponse { + pub count: i64, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LogEntry { pub timestamp: u64, @@ -326,3 +342,15 @@ impl From> for ValidatorFee { } } } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetBlockRequest { + pub block_id: BlockId, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetBlocksRequest { + pub limit: u64, + pub offset: u64, + pub ordering: Option, +} diff --git a/dan_layer/state_store_sqlite/src/reader.rs b/dan_layer/state_store_sqlite/src/reader.rs index 7f27ec323..80e2264ad 100644 --- a/dan_layer/state_store_sqlite/src/reader.rs +++ b/dan_layer/state_store_sqlite/src/reader.rs @@ -548,6 +548,64 @@ impl StateStoreReadTransa .collect() } + fn blocks_get_paginated( + &mut self, + limit: u64, + offset: u64, + asc_desc_created_at: Option, + ) -> Result>, StorageError> { + use crate::schema::{blocks, quorum_certificates}; + + let mut query = blocks::table + .left_join(quorum_certificates::table.on(blocks::qc_id.eq(quorum_certificates::qc_id))) + .select((blocks::all_columns, quorum_certificates::all_columns.nullable())) + .into_boxed(); + + if let Some(ordering) = asc_desc_created_at { + match ordering { + Ordering::Ascending => query = query.order_by(blocks::created_at.asc()), + Ordering::Descending => query = query.order_by(blocks::created_at.desc()), + } + } + + let blocks = query + .limit(limit as i64) + .offset(offset as i64) + .get_results::<(sql_models::Block, Option)>(self.connection()) + .map_err(|e| SqliteStorageError::DieselError { + operation: "blocks_get_paginated", + source: e, + })?; + + blocks + .into_iter() + .map(|(block, qc)| { + let qc = qc.ok_or_else(|| SqliteStorageError::DbInconsistency { + operation: "blocks_get_by_parent", + details: format!( + "block {} references non-existent quorum certificate {}", + block.id, block.qc_id + ), + })?; + + block.try_convert(qc) + }) + .collect() + } + + fn blocks_get_count(&mut self) -> Result { + use crate::schema::{blocks, quorum_certificates}; + let count = blocks::table + .left_join(quorum_certificates::table.on(blocks::qc_id.eq(quorum_certificates::qc_id))) + .select(diesel::dsl::count(blocks::id)) + .first::(self.connection()) + .map_err(|e| SqliteStorageError::DieselError { + operation: "blocks_get_count", + source: e, + })?; + Ok(count) + } + fn quorum_certificates_get(&mut self, qc_id: &QcId) -> Result, StorageError> { use crate::schema::quorum_certificates; diff --git a/dan_layer/state_store_sqlite/src/sql_models/block.rs b/dan_layer/state_store_sqlite/src/sql_models/block.rs index 18dd01a8e..2072ae083 100644 --- a/dan_layer/state_store_sqlite/src/sql_models/block.rs +++ b/dan_layer/state_store_sqlite/src/sql_models/block.rs @@ -52,6 +52,7 @@ impl Block { self.is_dummy, self.is_processed, self.is_committed, + self.created_at, )) } } diff --git a/dan_layer/storage/Cargo.toml b/dan_layer/storage/Cargo.toml index 3210c2229..0f7b05023 100644 --- a/dan_layer/storage/Cargo.toml +++ b/dan_layer/storage/Cargo.toml @@ -24,3 +24,4 @@ log = "0.4" rand = "0.8" thiserror = "1" serde = "1.0" +time = { version = "0.3", features = ["serde"] } diff --git a/dan_layer/storage/src/consensus_models/block.rs b/dan_layer/storage/src/consensus_models/block.rs index 14e1b8764..b097484bd 100644 --- a/dan_layer/storage/src/consensus_models/block.rs +++ b/dan_layer/storage/src/consensus_models/block.rs @@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize}; use tari_common_types::types::{FixedHash, FixedHashSizeError}; use tari_dan_common_types::{hashing, optional::Optional, serde_with, Epoch, NodeAddressable, NodeHeight, ShardId}; use tari_transaction::TransactionId; +use time::PrimitiveDateTime; use super::QuorumCertificate; use crate::{ @@ -26,6 +27,7 @@ use crate::{ SubstateUpdate, Vote, }, + Ordering, StateStoreReadTransaction, StateStoreWriteTransaction, StorageError, @@ -55,6 +57,8 @@ pub struct Block { is_processed: bool, /// Flag that indicates that the block has been committed. is_committed: bool, + /// Timestamp when was this stored. + stored_at: Option, } impl Block { @@ -81,6 +85,7 @@ impl Block { is_dummy: false, is_processed: false, is_committed: false, + stored_at: None, }; block.id = block.calculate_hash().into(); block @@ -98,6 +103,7 @@ impl Block { is_dummy: bool, is_processed: bool, is_committed: bool, + created_at: PrimitiveDateTime, ) -> Self { Self { id, @@ -113,6 +119,7 @@ impl Block { is_dummy, is_processed, is_committed, + stored_at: Some(created_at), } } @@ -143,6 +150,7 @@ impl Block { is_dummy: false, is_processed: false, is_committed: true, + stored_at: None, } } @@ -325,6 +333,19 @@ impl Block { tx.blocks_insert(self) } + pub fn get_paginated>( + tx: &mut TTx, + limit: u64, + offset: u64, + ordering: Option, + ) -> Result, StorageError> { + tx.blocks_get_paginated(limit, offset, ordering) + } + + pub fn get_count>(tx: &mut TTx) -> Result { + tx.blocks_get_count() + } + /// Inserts the block if it doesnt exist. Returns true if the block was saved and did not exist previously, /// otherwise false. pub fn save(&self, tx: &mut TTx) -> Result diff --git a/dan_layer/storage/src/state_store/mod.rs b/dan_layer/storage/src/state_store/mod.rs index 4a43952e6..3045a33f5 100644 --- a/dan_layer/storage/src/state_store/mod.rs +++ b/dan_layer/storage/src/state_store/mod.rs @@ -7,6 +7,7 @@ use std::{ ops::{Deref, DerefMut, RangeInclusive}, }; +use serde::{Deserialize, Serialize}; use tari_common_types::types::FixedHash; use tari_dan_common_types::{Epoch, NodeAddressable, ShardId}; use tari_transaction::{Transaction, TransactionId}; @@ -120,6 +121,13 @@ pub trait StateStoreReadTransaction { epoch_range: RangeInclusive, validator_public_key: Option<&Self::Addr>, ) -> Result>, StorageError>; + fn blocks_get_paginated( + &mut self, + limit: u64, + offset: u64, + asc_desc_created_at: Option, + ) -> Result>, StorageError>; + fn blocks_get_count(&mut self) -> Result; fn quorum_certificates_get(&mut self, qc_id: &QcId) -> Result, StorageError>; fn quorum_certificates_get_all<'a, I: IntoIterator>( @@ -295,7 +303,7 @@ pub trait StateStoreWriteTransaction { B: Borrow; } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum Ordering { Ascending, Descending,