From 9df306405228162501e85d3df166741ea3a60877 Mon Sep 17 00:00:00 2001 From: Elias Poroma Date: Sun, 19 Sep 2021 17:33:30 -0400 Subject: [PATCH] Add Block time page functionality --- README.md | 2 +- src/App.tsx | 3 - .../AddressBook/ViewFormatsModal.tsx | 8 +- .../AverageBlockTime/AverageBlockTime.tsx | 4 +- src/components/BlockAuthor/BlockAuthor.less | 12 + src/components/BlockAuthor/BlockAuthor.tsx | 106 +++-- src/components/BlockTime/BlockTime.less | 4 + src/components/BlockTime/BlockTime.tsx | 428 +++++++++++++++++- src/components/Config/AddEndpointModal.tsx | 3 +- src/components/Config/Configuration.tsx | 5 +- src/utils/UtilsFunctions.ts | 50 +- 11 files changed, 557 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index b58e0af..c1ec31b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Getting Started with Create React App +# Parachain utilities This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). diff --git a/src/App.tsx b/src/App.tsx index f03d57c..e9a9083 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,10 +12,8 @@ import BlockTime from "./components/BlockTime/BlockTime" import BlockAuthor from "./components/BlockAuthor/BlockAuthor" import { useAppDispatch, useAppSelector } from "./store/hooks" import { replaceText } from "./utils/UtilsFunctions" -import { setAddressList } from "./store/actions/addressActions" import { selectEndpoint, - setConfig, setEndpointList, } from "./store/actions/configActions" import { RPCEndpoint } from "./types" @@ -25,7 +23,6 @@ function App(): React.ReactElement { const config = useAppSelector(state => { return state.config }) - const addressList = useAppSelector(state => state.address.list) const dispatch = useAppDispatch() useEffect(() => { diff --git a/src/components/AddressBook/ViewFormatsModal.tsx b/src/components/AddressBook/ViewFormatsModal.tsx index 33dee81..697a3c9 100644 --- a/src/components/AddressBook/ViewFormatsModal.tsx +++ b/src/components/AddressBook/ViewFormatsModal.tsx @@ -1,10 +1,6 @@ -import { Button, Form, Input, message, Modal, Row, Space, Table } from "antd" -import React, { useEffect, useState } from "react" -import { addAddress } from "../../store/actions/addressActions" -import { useAppDispatch } from "../../store/hooks" +import { Button, message, Modal, Row, Space, Table } from "antd" +import React from "react" import { SubstrateAddress, TransformedSubstrateAddress } from "../../types" -import * as prefixes from "../../utils/ss58-registry.json" -import { encodeAddress, decodeAddress } from "@polkadot/util-crypto/address" import CopyToClipboard from "react-copy-to-clipboard" import { CopyOutlined } from "@ant-design/icons" diff --git a/src/components/AverageBlockTime/AverageBlockTime.tsx b/src/components/AverageBlockTime/AverageBlockTime.tsx index d3d474d..70c77ca 100644 --- a/src/components/AverageBlockTime/AverageBlockTime.tsx +++ b/src/components/AverageBlockTime/AverageBlockTime.tsx @@ -1,4 +1,4 @@ -import { BarChartOutlined, LineChartOutlined } from "@ant-design/icons" +import { BarChartOutlined } from "@ant-design/icons" import { ApiPromise, WsProvider } from "@polkadot/api" import { Button, @@ -200,7 +200,7 @@ function AverageBlockTime(): React.ReactElement { disabled={!isBlockRangeValid || isLoading} loading={isLoading} htmlType='submit'> - Calculate + Calculate Average Block Time diff --git a/src/components/BlockAuthor/BlockAuthor.less b/src/components/BlockAuthor/BlockAuthor.less index d678de2..ccee5fd 100644 --- a/src/components/BlockAuthor/BlockAuthor.less +++ b/src/components/BlockAuthor/BlockAuthor.less @@ -1,3 +1,15 @@ .block-author-container { padding: 25px; + + .collapse-blocks .ant-collapse-content-box { + padding: 0; + } + + .address-name { + font-weight: bold; + } + + .block-number { + font-weight: bold; + } } \ No newline at end of file diff --git a/src/components/BlockAuthor/BlockAuthor.tsx b/src/components/BlockAuthor/BlockAuthor.tsx index 6aca6b5..9e9ad36 100644 --- a/src/components/BlockAuthor/BlockAuthor.tsx +++ b/src/components/BlockAuthor/BlockAuthor.tsx @@ -1,10 +1,11 @@ -import { BarChartOutlined } from "@ant-design/icons" +import { BarChartOutlined, CaretRightOutlined } from "@ant-design/icons" import { ApiPromise, WsProvider } from "@polkadot/api" -import { BlockHash, Header } from "@polkadot/types/interfaces" import { Button, + Collapse, Form, InputNumber, + List, message, Row, Select, @@ -13,6 +14,7 @@ import { } from "antd" import React, { useState } from "react" import { useAppSelector } from "../../store/hooks" +import { findAuthorName } from "../../utils/UtilsFunctions" import "./BlockAuthor.less" interface BlockAuthorFormValues { @@ -34,6 +36,7 @@ interface BlockInfo { function BlockAuthor(): React.ReactElement { const [formBlocks] = Form.useForm() + const addresses = useAppSelector(state => state.address.list) const config = useAppSelector(state => state.config) const [results, setResults] = useState>([]) const [isBlockRangeValid, setIsBlockRangeValid] = useState(false) @@ -65,6 +68,7 @@ function BlockAuthor(): React.ReactElement { } const handleOnCalculate = (values: BlockAuthorFormValues) => { + setResults([]) countBlockAuthors(values) } @@ -86,58 +90,49 @@ function BlockAuthor(): React.ReactElement { const api = await ApiPromise.create({ provider }) // Load hashes + const groupedBlocks: Record = {} let loadedUntil = startBlock - let allHashes: BlockHash[] = [] while (loadedUntil <= endBlock) { let promiseCount = 0 const allowedParallel = 5 - const promises = [] - while (loadedUntil <= endBlock && promiseCount < allowedParallel) { - promises.push(api.rpc.chain.getBlockHash(loadedUntil)) - loadedUntil += 1 + let promises = [] + while ( + promiseCount < allowedParallel && + loadedUntil + promiseCount <= endBlock + ) { + promises.push(api.rpc.chain.getBlockHash(loadedUntil + promiseCount)) promiseCount += 1 } const newHashes = await Promise.all(promises) - allHashes = allHashes.concat(newHashes) - } - - // Load headers/authors - loadedUntil = startBlock - let allHeaders: any[] = [] - while (loadedUntil <= endBlock) { - let promiseCount = 0 - const allowedParallel = 5 - const promises = [] - while (loadedUntil <= endBlock && promiseCount < allowedParallel) { - promises.push( - api.derive.chain.getHeader(allHashes[loadedUntil - startBlock]) - ) - loadedUntil += 1 - promiseCount += 1 + promises = [] + for (const hash of newHashes) { + promises.push(api.derive.chain.getHeader(hash)) } const newHeaders = await Promise.all(promises) - allHeaders = allHeaders.concat(newHeaders) + newHeaders.forEach((header, index) => { + const author = header?.author?.toString() + if (author) { + groupedBlocks[author] = [ + ...(groupedBlocks[author] || []), + { + number: loadedUntil + index, + hash: newHashes[index].toString(), + }, + ] + } + }) + loadedUntil += newHashes.length } - const groupBlocks: Record = {} - for (const header of allHeaders) { - const author = header?.author?.toString() - if (author) { - groupBlocks[author] = [ - ...(groupBlocks[author] || []), - { - number: 1, - hash: "", - }, - ] - } - } + provider.disconnect() + const finalResults: BlockAuthorResult[] = [] - for (const author in groupBlocks) { + for (const author in groupedBlocks) { finalResults.push({ authorAddress: author, - blocks: groupBlocks[author], + authorName: findAuthorName(addresses, author), + blocks: groupedBlocks[author], }) } @@ -152,11 +147,38 @@ function BlockAuthor(): React.ReactElement { } const renderAuthor = (row: BlockAuthorResult) => { - return
{row.authorAddress}
+ return ( + <> + {row.authorName && {row.authorName}} + {row.authorAddress} + + ) } const renderBlocks = (row: BlockAuthorResult) => { - return
{row.blocks.length} blocks
+ return ( + ( + + )}> + + ( + + {item.number} -{" "} + {item.hash} + + )} + /> + + + ) } const columns = [ @@ -234,7 +256,7 @@ function BlockAuthor(): React.ReactElement { disabled={!isBlockRangeValid || isLoading} loading={isLoading} htmlType='submit'> - Calculate + Load Block Authors diff --git a/src/components/BlockTime/BlockTime.less b/src/components/BlockTime/BlockTime.less index 2394bb0..47e7b0f 100644 --- a/src/components/BlockTime/BlockTime.less +++ b/src/components/BlockTime/BlockTime.less @@ -1,3 +1,7 @@ .block-time-container { padding: 25px; + .default-block-time { + font-size: 12px; + color: gray; + } } \ No newline at end of file diff --git a/src/components/BlockTime/BlockTime.tsx b/src/components/BlockTime/BlockTime.tsx index be6a5fd..1eded38 100644 --- a/src/components/BlockTime/BlockTime.tsx +++ b/src/components/BlockTime/BlockTime.tsx @@ -1,8 +1,432 @@ -import React from "react" +import { BarChartOutlined } from "@ant-design/icons" +import { ApiPromise, WsProvider } from "@polkadot/api" +import { + Button, + DatePicker, + Form, + InputNumber, + message, + Row, + Space, + Spin, + Table, +} from "antd" +import { Moment } from "moment" +import React, { useEffect, useState } from "react" +import { useAppSelector } from "../../store/hooks" +import { formatDate, toUnixTimestamp } from "../../utils/UtilsFunctions" import "./BlockTime.less" +interface BlockTimeFormValues { + datetime: Moment + blockNumber: number + expectedBlockTime: number +} + +interface BlockTimeResult { + chainName: string + endpoint: string + estimateResult?: number | string + type: string +} + function BlockTime(): React.ReactElement { - return
Placeholder BlockTime
+ const [formBlocks] = Form.useForm() + const config = useAppSelector(state => state.config) + const [results, setResults] = useState>([]) + const [isOptionalFieldsValid, setIsOptionalFieldsValid] = useState(false) + const [isExpectedTimeLoading, setIsExpectedTimeLoading] = useState(false) + const [defaultBlockTime, setDefaultBlockTime] = useState() + const [isLoading, setIsLoading] = useState(false) + + useEffect(() => { + loadExpectedBlockTime() + }, []) + + const loadExpectedBlockTime = async () => { + try { + setIsExpectedTimeLoading(true) + // Connect to chain + const provider = new WsProvider(config.selectedEndpoint?.value) + + provider.on("error", () => { + provider.disconnect() + message.error("An error ocurred when trying to connect to the endpoint") + setIsExpectedTimeLoading(false) + }) + + // Create the API + const api = await ApiPromise.create({ provider }) + + // Get default block time + const timeMs = api.consts.babe.expectedBlockTime.toNumber() + provider.disconnect() + + if (!formBlocks.getFieldValue("expectedBlockTime")) { + formBlocks.setFieldsValue({ + expectedBlockTime: timeMs, + }) + } + setDefaultBlockTime(timeMs) + setIsExpectedTimeLoading(false) + } catch (err) { + console.log(err) + message.error("An error ocurred when trying to load expected block time.") + setIsExpectedTimeLoading(false) + } + } + + const checkOptionalFields = ( + changedValues: Record, + values: BlockTimeFormValues + ) => { + // Validate that start block is less than end block + if (changedValues && !values.blockNumber && !values.datetime) { + setIsOptionalFieldsValid(false) + formBlocks.setFields([ + { + name: "blockNumber", + value: values.blockNumber, + errors: ["Enter block number or datetime"], + }, + { + name: "datetime", + value: values.datetime, + errors: ["Enter block number or datetime"], + }, + ]) + } else { + setIsOptionalFieldsValid(true) + formBlocks.setFields([ + { + name: "blockNumber", + value: values.blockNumber, + errors: [], + }, + { + name: "datetime", + value: values.datetime, + errors: [], + }, + ]) + formBlocks.validateFields() + } + } + + const handleOnCalculate = (values: BlockTimeFormValues) => { + calculateAverageBlockTime(values) + } + + const calculateAverageBlockTime = async (values: BlockTimeFormValues) => { + try { + const { blockNumber, datetime, expectedBlockTime } = values + setResults([]) + + if (blockNumber) { + estimateForBlockNumber(blockNumber, expectedBlockTime) + } else { + estimateForDateTime(datetime, expectedBlockTime) + } + } catch (err) { + console.log(err) + message.error("An error ocurred when loading block data.") + setIsLoading(false) + } + } + + const estimateForBlockNumber = async ( + blockNumber: number, + expectedBlockTime: number + ) => { + setIsLoading(true) + const auxResults: BlockTimeResult[] = [] + + const enabledEndpoints = config.endpoints.filter( + endpoint => endpoint.enabled + ) + let index = 0 + while (index < enabledEndpoints.length) { + try { + // Connect to chain + const provider = new WsProvider(enabledEndpoints[index].value) + + provider.on("error", () => { + provider.disconnect() + message.error( + "An error ocurred when trying to connect to the endpoint" + ) + setIsLoading(false) + }) + + // Create the API + const api = await ApiPromise.create({ provider }) + + // Get current block number + const latestBlock = await api.rpc.chain.getHeader() + const currentBlockNumber = latestBlock.number.toNumber() + + let formattedResult = "" + let type = "" + + // If it is future, estimate the date time + if (blockNumber > currentBlockNumber) { + const currentHash = await api.rpc.chain.getBlockHash( + currentBlockNumber + ) + const currentTime = await api.query.timestamp.now.at(currentHash) + const estimatedTime = + currentTime.toNumber() + + expectedBlockTime * (blockNumber - currentBlockNumber) + formattedResult = formatDate(estimatedTime, config.utcTime) + type = "Future" + + // If it is past, fetch the blocktime + } else { + const pastHash = await api.rpc.chain.getBlockHash(blockNumber) + const pastTime = await api.query.timestamp.now.at(pastHash) + formattedResult = formatDate(pastTime.toNumber(), config.utcTime) + type = "Past" + } + + provider.disconnect() + + // Add estimate to results + auxResults.push({ + chainName: enabledEndpoints[index].chainName, + endpoint: enabledEndpoints[index].value, + estimateResult: formattedResult, + type, + }) + + index += 1 + } catch (err) { + console.log(err) + index += 1 + } + } + setResults(auxResults) + setIsLoading(false) + } + + const estimateForDateTime = async ( + datetime: Moment, + expectedBlockTime: number + ) => { + setIsLoading(true) + const inputTimestamp = toUnixTimestamp(datetime, config.utcTime) + + const enabledEndpoints = config.endpoints.filter( + endpoint => endpoint.enabled + ) + let index = 0 + while (index < enabledEndpoints.length) { + try { + // Connect to chain + const provider = new WsProvider(enabledEndpoints[index].value) + + provider.on("error", () => { + provider.disconnect() + message.error( + "An error ocurred when trying to connect to the endpoint" + ) + setIsLoading(false) + }) + + // Create the API + const api = await ApiPromise.create({ provider }) + + // Get current block number + const latestBlock = await api.rpc.chain.getHeader() + const currentBlockNumber = latestBlock.number.toNumber() + const currentHash = await api.rpc.chain.getBlockHash(currentBlockNumber) + const currentTime = ( + await api.query.timestamp.now.at(currentHash) + ).toNumber() + + let result: number + let type = "" + + // If it is future, estimate the block number + if (inputTimestamp > currentTime) { + result = + currentBlockNumber + + Math.ceil((inputTimestamp - currentTime) / expectedBlockTime) + type = "Future" + + // If it is past, find the block number + } else { + result = await searchPastBlockNumber( + api, + inputTimestamp, + currentTime, + currentBlockNumber, + expectedBlockTime + ) + type = "Past" + } + + provider.disconnect() + + // Add estimate to results + setResults(oldResults => { + return [ + ...oldResults, + { + chainName: enabledEndpoints[index].chainName, + endpoint: enabledEndpoints[index].value, + estimateResult: result, + type, + }, + ] + }) + + index += 1 + } catch (err) { + console.log(err) + index += 1 + } + } + setIsLoading(false) + } + + const searchPastBlockNumber = async ( + api: ApiPromise, + targetTime: number, + currentTime: number, + currentBlockNumber: number, + expectedBlockTime: number + ): Promise => { + // If times match, return + if (targetTime === currentTime) { + return currentBlockNumber + } + + // Try to estimate target block number, minimum value is 1 + let searchBlockNumber = Math.max( + 1, + currentBlockNumber - + Math.ceil((currentTime - targetTime) / expectedBlockTime) + ) + + let searchHash = await api.rpc.chain.getBlockHash(searchBlockNumber) + let searchTime = (await api.query.timestamp.now.at(searchHash)).toNumber() + + // If estimated search time matches target, return value + if (searchTime === targetTime) { + return searchBlockNumber + } + + // Navigate until finding the block + const directionIncreasing = searchTime < targetTime + while (searchBlockNumber > 0 && searchBlockNumber < currentBlockNumber) { + // Navigate 1 block + const newBlockNumber = searchBlockNumber + (directionIncreasing ? 1 : -1) + + // Load new block time + const newHash = await api.rpc.chain.getBlockHash(newBlockNumber) + const newTime = (await api.query.timestamp.now.at(newHash)).toNumber() + + // If we found the block, return the value + if (directionIncreasing && newTime >= targetTime) return newBlockNumber + if (!directionIncreasing && newTime < targetTime) return searchBlockNumber + + // If we didn't find it, update search and continue + searchBlockNumber = newBlockNumber + searchHash = newHash + searchTime = newTime + } + + return 1 + } + + const renderChain = (row: BlockTimeResult) => { + return ( +
+ {row.chainName} ({row.endpoint}) +
+ ) + } + + const renderResult = (row: BlockTimeResult) => { + return
{row.estimateResult}
+ } + + const columns = [ + { + title: "Chain", + key: "chain", + render: renderChain, + }, + { + title: "Type", + key: "type", + dataIndex: "type", + }, + { + title: "Estimated", + key: "action", + render: renderResult, + }, + ] + + return ( +
+
+ + + + + + + + + + + + + + + {isExpectedTimeLoading && ( +
+ +
+ )} + {defaultBlockTime && ( +
+ Default value: {defaultBlockTime} ms +
+ )} +
+ + + +
+

Result:

+ + + ) } export default BlockTime diff --git a/src/components/Config/AddEndpointModal.tsx b/src/components/Config/AddEndpointModal.tsx index a9709aa..4583e32 100644 --- a/src/components/Config/AddEndpointModal.tsx +++ b/src/components/Config/AddEndpointModal.tsx @@ -1,10 +1,9 @@ import { ApiPromise, WsProvider } from "@polkadot/api" import { Button, Form, Input, Modal, Row, Space, message, Spin } from "antd" import React, { useEffect, useState } from "react" -import { addAddress } from "../../store/actions/addressActions" import { addEndpoint } from "../../store/actions/configActions" import { useAppDispatch, useAppSelector } from "../../store/hooks" -import { RPCEndpoint, SubstrateAddress } from "../../types" +import { RPCEndpoint } from "../../types" type AddEndpointModalProps = { showModal: boolean diff --git a/src/components/Config/Configuration.tsx b/src/components/Config/Configuration.tsx index a1b2a5b..3920c4b 100644 --- a/src/components/Config/Configuration.tsx +++ b/src/components/Config/Configuration.tsx @@ -1,7 +1,6 @@ import React, { useState } from "react" import { Button, - Checkbox, Divider, message, Row, @@ -19,8 +18,6 @@ import { } from "../../store/actions/configActions" import { RPCEndpoint } from "../../types" import { CheckOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons" -import { formatDate } from "@polkadot/types/node_modules/@polkadot/util" -import { transformDate } from "../../utils/UtilsFunctions" import AddEndpointModal from "./AddEndpointModal" function Configuration(): React.ReactElement { @@ -92,7 +89,7 @@ function Configuration(): React.ReactElement { {config?.selectedEndpoint?.value === row.value ? ( ) : (