commit 5cae66ee5a60caedfceb951db45293ffaddad4f5 Author: jenya Date: Sun Jun 27 22:21:09 2021 +0300 1.01 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f1ca08f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +**/*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +node_modules +dist + +yarn-error.log + +.idea +.vscode + +Dockerfile +.DS_Store +.gitignore +.git \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f7720f1 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +REACT_APP_RPC_URL_SHARD0=https://a.api.s0.t.hmny.io +REACT_APP_RPC_URL_SHARD1=https://api.s1.t.hmny.io +REACT_APP_RPC_URL_SHARD2=https://api.s2.t.hmny.io +REACT_APP_RPC_URL_SHARD3=https://api.s3.t.hmny.io +REACT_APP_AVAILABLE_SHARDS=0 +REACT_APP_EXPLORER_V1_API_URL=https://explorer.harmony.one:8888 +REACT_APP_INDEXER_IPFS_GATEWAY=https://ipfs.io/ipfs/ +REACT_APP_PROD_ADDRESS=https://explorer-v2-api.hmny.io +REACT_APP_DEV_ADDRESS=https://ws.explorer-v2.hmny.io diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3658848 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.firebase/* +/.firebase + + + +.cache + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +yarn.lock +*.iml +.firebaserc +firebase.json +firebase-debug.log +.firebase +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..25c8dee --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM node:14.4-alpine + +RUN apk add --no-cache openssl + +WORKDIR /usr/src/app +COPY . . + +RUN npx yarn install +RUN npx yarn run build + +EXPOSE 3000 + +CMD ["npx", "yarn", "run" ,"start"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b58e0af --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Getting Started with Create React App + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `yarn start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.\ +You will also see any lint errors in the console. + +### `yarn test` + +Launches the test runner in the interactive watch mode.\ +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `yarn build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.\ +Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `yarn eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). diff --git a/package.json b/package.json new file mode 100644 index 0000000..84e2496 --- /dev/null +++ b/package.json @@ -0,0 +1,66 @@ +{ + "name": "harmony-explorer-v2-frontend", + "version": "0.1.0", + "private": true, + "dependencies": { + "@metamask/detect-provider": "^1.2.0", + "big.js": "^6.1.1", + "dayjs": "^1.10.4", + "grommet": "2.17.2", + "grommet-icons": "^4.5.0", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-responsive": "^8.2.0", + "react-router-dom": "^5.2.0", + "react-singleton-hook": "^3.1.1", + "react-virtualized-auto-sizer": "^1.0.5", + "react-window": "^1.8.6", + "socket.io-client": "^4.0.1", + "styled-components": "^5.2.3", + "web3": "^1.3.6" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@testing-library/jest-dom": "^5.11.4", + "@testing-library/react": "^11.1.0", + "@testing-library/user-event": "^12.1.10", + "@types/big.js": "^6.0.2", + "@types/jest": "^26.0.15", + "@types/node": "^12.0.0", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", + "@types/react-responsive": "^8.0.2", + "@types/react-router-dom": "^5.1.7", + "@types/react-virtualized-auto-sizer": "^1.0.0", + "@types/react-window": "^1.8.3", + "@types/styled-components": "^5.1.9", + "js-sha3": "^0.8.0", + "prettier": "^2.2.1", + "react-scripts": "4.0.3", + "typescript": "^4.1.2", + "web-vitals": "^1.0.1" + } +} diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..829eda8 --- /dev/null +++ b/public/404.html @@ -0,0 +1,33 @@ + + + + + + Page Not Found + + + + +
+

404

+

Page Not Found

+

The specified file was not found on this website. Please check the URL for mistakes and try again.

+

Why am I seeing this?

+

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

+
+ + diff --git a/public/env.js b/public/env.js new file mode 100644 index 0000000..49e40f4 --- /dev/null +++ b/public/env.js @@ -0,0 +1 @@ +window.env = {}; \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..7172abd Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..3ec161d --- /dev/null +++ b/public/index.html @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + Harmony Blockchain Explorer + + + + +
+ + + diff --git a/public/logo192.png b/public/logo192.png new file mode 100644 index 0000000..fc44b0a Binary files /dev/null and b/public/logo192.png differ diff --git a/public/logo512.png b/public/logo512.png new file mode 100644 index 0000000..a4e47a6 Binary files /dev/null and b/public/logo512.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..080d6c7 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..fa769e1 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,81 @@ +import React, { useEffect } from "react"; +import "./index.css"; +import { Box, Grommet } from "grommet"; +import { BrowserRouter as Router, useHistory } from "react-router-dom"; + +import { Routes } from "src/Routes"; +import { AppHeader } from "src/components/appHeader"; +import { AppFooter } from "src/components/appFooter"; + +import { SearchInput, BaseContainer } from "src/components/ui"; +import { ONE_USDT_Rate } from "src/components/ONE_USDT_Rate"; +import { ERC20_Pool } from "src/components/ERC20_Pool"; +import { ERC721_Pool } from "src/components/ERC721_Pool"; +import { ERC1155_Pool } from "src/components/ERC1155_Pool"; +import { useThemeMode } from "src/hooks/themeSwitcherHook"; +import { theme, darkTheme } from "./theme"; +import { Toaster, ToasterComponent } from "./components/ui/toaster"; + +export const toaster = new Toaster(); + +function App() { + return ( + + + + ); +} + +let prevAddress = document.location.pathname; + +function AppWithHistory() { + const themeMode = useThemeMode(); + const history = useHistory(); + + useEffect(() => { + const unlisten = history.listen((location, action) => { + if (prevAddress !== location.pathname) { + prevAddress = location.pathname; + const scrollBody = document.getElementById("scrollBody"); + if (scrollBody) { + scrollBody.scrollTo({ top: 0 }); + } + } + }); + return () => { + unlisten(); + }; + }, []); + + document.body.className = themeMode; + + return ( + + + + + + + + + + + + + + + + + + ); +} + +export default App; diff --git a/src/Responive/breakpoints.ts b/src/Responive/breakpoints.ts new file mode 100644 index 0000000..d05fb10 --- /dev/null +++ b/src/Responive/breakpoints.ts @@ -0,0 +1,8 @@ +export const breakpoints = { + mobile: '375px', + mobileL: '425px', + tablet: '768px', + tabletM: '868px', + laptop: '1024px', + desktop: '1366px', +}; \ No newline at end of file diff --git a/src/Routes.tsx b/src/Routes.tsx new file mode 100644 index 0000000..645db5b --- /dev/null +++ b/src/Routes.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { Switch, Route, Redirect } from "react-router-dom"; +import { BlockPage } from "src/pages/BlockPage"; +import { MainPage } from "src/pages/MainPage"; +import { TransactionPage } from "src/pages/TransactionPage"; +import { StakingTransactionPage } from "src/pages/StackingTransactionPage"; +import { AllBlocksPage } from "src/pages/AllBlocksPage"; +import { AllTransactionsPage } from "src/pages/AllTransactionsPage"; +import { AddressPage } from "src/pages/AddressPage"; +import { ERC20List } from "src/pages/ERC20List"; +import { ERC721List } from "src/pages/ERC721List"; +import { VerifyContract } from "./pages/VerifyContract/VerifyContract"; +import { breakpoints } from "./Responive/breakpoints"; +import { useMediaQuery } from "react-responsive"; +import { ERC1155List } from "./pages/ERC1155List"; +import { InventoryDetailsPage } from "./pages/InventoryDetailsPage/InventoryDetailsPage"; + +export function Routes() { + const isLessTablet = useMediaQuery({ maxDeviceWidth: breakpoints.tablet }); + + return ( + <> + + + + + + + {/* */} + + + + + + + + + + + + + {/* */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/api/ApiCache.ts b/src/api/ApiCache.ts new file mode 100644 index 0000000..5e9a6ad --- /dev/null +++ b/src/api/ApiCache.ts @@ -0,0 +1,22 @@ +export class ApiCache { + public key: string = "apiCache"; + + constructor(props: { key: string }) { + this.key = props.key; + const objStr = localStorage.getItem(this.key); + if (!objStr) { + localStorage.setItem(this.key, "{}"); + } + } + + get(name: string) { + const obj = JSON.parse(localStorage.getItem(this.key) as string); + return obj[name] as T | undefined; + } + + set(name: string, value: any) { + const obj = JSON.parse(localStorage.getItem(this.key) as string); + + localStorage.setItem(this.key, JSON.stringify({ ...obj, [name]: value })); + } +} diff --git a/src/api/client.interface.ts b/src/api/client.interface.ts new file mode 100644 index 0000000..122e4ec --- /dev/null +++ b/src/api/client.interface.ts @@ -0,0 +1,57 @@ +export interface IUserERC721Assets { + lastUpdateBlockNumber: null | number; + meta?: { + attributes: { value: string; trait_type: string }[]; + collection_id: string; + collection_url: string; + core: any; + description: string; + external_url: string; + id: string; + image: string; + license: string; + name: string; + type?: string + youtube_url?: string; + symbol?: string; + external_link?: string; + }; + needUpdate: boolean; + ownerAddress: string; + tokenAddress: string; + tokenID: string; + tokenURI: string; + type?: string +} + +export type TRelatedTransaction = + | "transaction" + | "staking_transaction" + | "internal_transaction" + | "erc20" + | "erc1155" + | "erc721"; + +export interface IPairPrice { + askPrice: string; + askQty: string; + bidPrice: string; + bidQty: string; + closeTime: number; + count: number; + firstId: number; + highPrice: string; + lastId: number; + lastPrice: string; + lastQty: string; + lowPrice: string; + openPrice: string; + openTime: number; + prevClosePrice: string; + priceChange: string; + priceChangePercent: string; + quoteVolume: string; + symbol: string; + volume: string; + weightedAvgPrice: string; +} diff --git a/src/api/client.ts b/src/api/client.ts new file mode 100644 index 0000000..7e6d632 --- /dev/null +++ b/src/api/client.ts @@ -0,0 +1,166 @@ +import { transport } from "./explorer"; +import { + Block, + InternalTransaction, + RPCStakingTransactionHarmony, + RPCTransactionHarmony, + RelatedTransaction, +} from "src/types"; +import { + IPairPrice, + IUserERC721Assets, + TRelatedTransaction, +} from "./client.interface"; +import { ApiCache } from "./ApiCache"; +// import { ClientCache } from "./clientCache"; + +// const clientCache = new ClientCache({ +// timer: 5000, // 15 mins +// }); + +// TODO: hardcode +let pairCache: { [pair: string]: IPairPrice } = {}; +setInterval(() => { + pairCache = {}; +}, 90000); + +const signatureHash = new ApiCache({ key: "signatureHashCache" }); + +export function getBlockByNumber(params: any[]) { + return transport("getBlockByNumber", params) as Promise; +} + +export function getBlockByHash(params: any[]) { + return transport("getBlockByHash", params) as Promise; +} + +export function getBlocks(params: any[]) { + return transport("getBlocks", params) as Promise; +} + +export function getCount(params: any[]) { + return transport("getCount", params) as Promise<{ count: string }>; +} + +export function getTransactions(params: any[]) { + return transport("getTransactions", params) as Promise< + RPCTransactionHarmony[] + >; +} + +export function getTransactionByField(params: any[]) { + return transport( + "getTransactionByField", + params + ) as Promise; +} + +export function getStakingTransactionByField(params: [number, "hash", string]) { + return transport( + "getStakingTransactionsByField", + params + ) as Promise; +} + +export function getInternalTransactionsByField(params: any[]) { + return transport("getInternalTransactionsByField", params) as Promise< + InternalTransaction[] + >; +} + +export function getTransactionLogsByField(params: any[]) { + return transport("getLogsByField", params) as Promise; +} + +export function getByteCodeSignatureByHash(params: [string]) { + return signatureHash.get(params[0]) + ? Promise.resolve(signatureHash.get(params[0])) + : (transport("getSignaturesByHash", params).then((res) => { + signatureHash.set(params[0], res); + + return Promise.resolve(res); + }) as Promise); +} + +export function getRelatedTransactions(params: any[]) { + return transport("getRelatedTransactions", params) as Promise< + RelatedTransaction[] + >; +} + +export function getTransactionCountLast14Days() { + return transport("getTransactionCountLast14Days", []) as Promise; +} + +export function getContractsByField(params: any[]) { + return transport("getContractsByField", params) as Promise; +} + +export function getAllERC20() { + return transport("getAllERC20", []) as Promise; +} + +export function getAllERC721() { + return transport("getAllERC721", []) as Promise; +} + +export function getAllERC1155() { + return transport("getAllERC1155", []) as Promise; +} + +export function getUserERC20Balances(params: any[]) { + return transport("getUserERC20Balances", params) as Promise; +} + +export function getUserERC721Assets(params: any[]) { + return transport("getUserERC721Assets", params) as Promise< + IUserERC721Assets[] + >; +} + +export function getTokenERC721Assets(params: [string]) { + return transport("getTokenERC721Assets", params) as Promise< + IUserERC721Assets[] + >; +} + +export function getTokenERC1155Assets(params: [string]) { + return transport("getTokenERC1155Assets", params) as Promise< + IUserERC721Assets[] + >; +} + +export function getUserERC1155Balances(params: [string]) { + return transport("getUserERC1155Balances", params) as Promise< + { + tokenID: string; + ownerAddress: string; + tokenAddress: string; + amount: string; + needUpdate: boolean; + lastUpdateBlockNumber: number | null; + }[] + >; +} + +export function getRelatedTransactionsByType( + params: [0, string, TRelatedTransaction, any] +) { + return transport("getRelatedTransactionsByType", params) as Promise< + RelatedTransaction[] + >; +} + +export function getBinancePairPrice(params: [string]) { + const cacheValue = pairCache[params[0]]; + return cacheValue + ? Promise.resolve(cacheValue) + : transport("getBinancePairPrice", params).then((res) => { + pairCache[params[0]] = res; + return res; + }); +} + +export function getBinancePairHistoricalPrice(params: [string]) { + return transport("getBinancePairHistoricalPrice", params) as Promise; +} diff --git a/src/api/clientCache.ts b/src/api/clientCache.ts new file mode 100644 index 0000000..df29351 --- /dev/null +++ b/src/api/clientCache.ts @@ -0,0 +1,73 @@ +import { SettingsService } from "src/utils/settingsService/SettingsService"; + +export interface IClientCacheProps { + timer?: number; // ms + key?: string; +} + +export class ClientCache { + public readonly key; + private lastCacheTime = new Date().getTime(); + + private keysMap: { [key: string]: number } = {}; + + private settigsService; + + constructor(props: IClientCacheProps) { + this.key = props.key || "ClientCache"; + this.settigsService = new SettingsService(this.key); + + this.lastCacheTime = this.getTime(); + + if (props.timer) { + setInterval(() => { + Object.keys(this.keysMap).forEach((key) => { + const currentDate = new Date().getTime(); + if ( + currentDate - (props.timer as number) > this.lastCacheTime && + key !== `${this.key}_time_${this.key}` + ) { + this.remove(key); + console.log("remove" + key); + } + }); + + this.lastCacheTime = new Date().getTime(); + this.regTime(); + }, props.timer); + } + } + + private regTime() { + this.settigsService.set(`time_${this.key}`, { + lastCacheTime: this.lastCacheTime, + }); + } + + private getTime() { + return this.settigsService.get(`time_${this.key}`, { + lastCacheTime: new Date().getTime(), + }); + } + + get( + key: string, + defaultValue?: any + ): T | string | typeof defaultValue { + const item = this.settigsService.get(`${key}`, defaultValue); + return item; + } + + set(key: string, value: string | object) { + this.settigsService.set(key, value); + } + + remove(key: string) { + try { + localStorage.removeItem(key); + delete this.keysMap[key]; + } catch { + return; + } + } +} diff --git a/src/api/explorer/index.ts b/src/api/explorer/index.ts new file mode 100644 index 0000000..c3a1b2b --- /dev/null +++ b/src/api/explorer/index.ts @@ -0,0 +1 @@ +export {transport} from './ws' \ No newline at end of file diff --git a/src/api/explorer/ws/index.ts b/src/api/explorer/ws/index.ts new file mode 100644 index 0000000..fa5c4e0 --- /dev/null +++ b/src/api/explorer/ws/index.ts @@ -0,0 +1,25 @@ +import { io } from "socket.io-client"; + +const socket = io(process.env.REACT_APP_DEV_ADDRESS as string, { + transports: ["websocket"], +}); + +socket.connect(); + +export const transport = (method: string, params: any[]) => { + return new Promise((resolve, reject) => { + socket.emit(method, params, (res: any) => { + try { + const payload = JSON.parse(res.payload); + + if (res.event === "Response") { + resolve(payload); + } else { + reject(payload); + } + } catch (err) { + reject(null); + } + }); + }); +}; diff --git a/src/api/explorerV1.ts b/src/api/explorerV1.ts new file mode 100644 index 0000000..7b83656 --- /dev/null +++ b/src/api/explorerV1.ts @@ -0,0 +1,91 @@ +import { AbiItem } from "web3-utils"; + +export interface IVerifyContractData { + contractAddress: string; + compiler: string; + optimizer: string; + optimizerTimes: string; + sourceCode: string; + libraries: { value: string; id: string }[]; + constructorArguments: string; + chainType: string; + contractName: string; + statusText: string; + isLoading: boolean; +} + +export interface IVerifyContractDataSendData { + contractAddress: string; + compiler: string; + optimizer: string; + optimizerTimes: string; + sourceCode: string; + libraries: string[]; + constructorArguments: string; + chainType: string; + contractName: string; +} + +export const verifyContractCode = async (data: IVerifyContractDataSendData) => { + const response = await fetch( + `${process.env.REACT_APP_EXPLORER_V1_API_URL}/codeVerification`, + { + method: "POST", + mode: "cors", + cache: "no-cache", + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + }, + redirect: "follow", + referrerPolicy: "no-referrer", + body: JSON.stringify(data), + } + ); + + return await response.json(); +}; + +export const loadSourceCode = async (address: string): Promise => { + const response = await fetch( + `${process.env.REACT_APP_EXPLORER_V1_API_URL}/fetchContractCode?contractAddress=${address}`, + { + mode: "cors", + cache: "no-cache", + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + }, + redirect: "follow", + referrerPolicy: "no-referrer", + } + ); + + // return { + // contractAddress: "one1shend7cl77j77cud0ga464xsqcq7kkveg7z88r", + // compiler: "0.4.26", + // optimizer: "No", + // optimizerTimes: "0", + // sourceCode: + // "pragma solidity ^0.4.17;contract Lottery { address public manager; address[] public players; function Lottery() public { manager = msg.sender; } function enter() public payable { require(msg.value > 0.01 ether); players.push(msg.sender); } function random() private view returns (uint256) { return uint256(keccak256(block.difficulty, now, players)); } function pickWinner() public restricted { uint256 index = random() % players.length; players[index].transfer(this.balance); players = new address[](0); } modifier restricted() { require(msg.sender == manager); _; } function getPlayers() public view returns (address[]) { return players; }}", + // libraries: ["", "", "", "", ""], + // constructorArguments: "", + // chainType: "testnet", + // contractName: "Lottery", + // }; + + return await response.json(); +}; + +export interface ISourceCode { + contractAddress: string; + compiler: string; + optimizer: string; + optimizerTimes: string; + sourceCode: string; + libraries: string[]; + constructorArguments: string; + chainType: string; + contractName: string; + abi?: AbiItem[]; +} diff --git a/src/api/rpc.ts b/src/api/rpc.ts new file mode 100644 index 0000000..c5186ff --- /dev/null +++ b/src/api/rpc.ts @@ -0,0 +1,83 @@ +export type TRPCResponse = { id: number; jsonrpc: "2.0"; result: T }; + +export const rpcAdapter = (...args: Parameters) => { + /** + * wrapper for fetch. for some middleware in future requests + */ + + return (fetch + .apply(window, args) + .then((res) => res.json()) as unknown) as Promise; +}; + +export const getBalance = (params: [string, "latest"]) => { + return rpcAdapter>("https://api.s0.t.hmny.io/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "eth_getBalance", + id: 1, + params, + }), + }); +}; + +export const getAllBalance = (params: [string, "latest"]) => { + return Promise.all([ + rpcAdapter>( + `${process.env["REACT_APP_RPC_URL_SHARD0"]}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "eth_getBalance", + id: 1, + params, + }), + } + ), + rpcAdapter>( + `${process.env["REACT_APP_RPC_URL_SHARD1"]}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "eth_getBalance", + id: 1, + params, + }), + } + ), + rpcAdapter>( + `${process.env["REACT_APP_RPC_URL_SHARD2"]}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "eth_getBalance", + id: 1, + params, + }), + } + ), + rpcAdapter>( + `${process.env["REACT_APP_RPC_URL_SHARD3"]}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "eth_getBalance", + id: 1, + params, + }), + } + ), + ]).then((arr) => { + return Promise.resolve(arr.map((item) => item.result)); + }); +}; diff --git a/src/assets/Logo.svg b/src/assets/Logo.svg new file mode 100644 index 0000000..0534c55 --- /dev/null +++ b/src/assets/Logo.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/components/BinancePairHistoricalPrice_Pool.tsx b/src/components/BinancePairHistoricalPrice_Pool.tsx new file mode 100644 index 0000000..4c1c782 --- /dev/null +++ b/src/components/BinancePairHistoricalPrice_Pool.tsx @@ -0,0 +1,37 @@ +import React, { useEffect } from "react"; +import { + Erc20Price, + setBinancePairHistoricalPrice, + useBinancePairHistoricalPrice, +} from "src/hooks/BinancePairHistoricalPrice"; + +export function BinancePairHistoricalPrice_Pool() { + useEffect(() => { + const getRates = async () => { + const erc20: Erc20Price[] = useBinancePairHistoricalPrice(); + const erc20MapPrice = {} as Record; + erc20.forEach((i) => { + erc20MapPrice[i.hrc20Address] = i; + }); + + window.localStorage.setItem( + "BinancePairHistoricalPrice", + JSON.stringify(erc20MapPrice) + ); + setBinancePairHistoricalPrice(erc20); + }; + + let tId = 0; + + setTimeout(() => { + getRates(); + tId = window.setInterval(getRates, 10 * 60 * 1e3); + }); + + return () => { + clearTimeout(tId); + }; + }, []); + + return null; +} diff --git a/src/components/ERC1155_Pool.tsx b/src/components/ERC1155_Pool.tsx new file mode 100644 index 0000000..daad619 --- /dev/null +++ b/src/components/ERC1155_Pool.tsx @@ -0,0 +1,31 @@ +import React, { useEffect } from "react"; +import { getAllERC1155 } from "src/api/client"; +import { setERC1155Pool, ERC1155 } from "src/hooks/ERC1155_Pool"; + +export function ERC1155_Pool() { + useEffect(() => { + const getRates = async () => { + const erc1155: ERC1155[] = await getAllERC1155(); + const erc1155Map = {} as Record; + erc1155.forEach((i: any) => { + erc1155Map[i.address] = i; + }); + + window.localStorage.setItem("ERC1155_Pool", JSON.stringify(erc1155Map)); + setERC1155Pool(erc1155Map); + }; + + let tId = 0; + + setTimeout(() => { + getRates(); + tId = window.setInterval(getRates, 10 * 60 * 1e3); + }); + + return () => { + clearTimeout(tId); + }; + }, []); + + return null; +} diff --git a/src/components/ERC20Value.tsx b/src/components/ERC20Value.tsx new file mode 100644 index 0000000..49ae3fa --- /dev/null +++ b/src/components/ERC20Value.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { useERC20Pool } from "src/hooks/ERC20_Pool"; +import Big from "big.js"; +import { formatNumber as _formatNumber } from "src/components/ui/utils"; + +export function ERC20Value(props: { + tokenAddress: string; + value: string | number; +}) { + const { tokenAddress, value } = props; + const erc20Map = useERC20Pool(); + + const tokenInfo: any = erc20Map[tokenAddress]; + + const bi = tokenInfo.decimals + ? Big(value).div(10 ** tokenInfo.decimals) + : value; + + const v = bi.toString(); + + return ( + <> + {v} {tokenInfo.symbol ? tokenInfo.symbol : ""} + + ); +} diff --git a/src/components/ERC20_Pool.tsx b/src/components/ERC20_Pool.tsx new file mode 100644 index 0000000..424097b --- /dev/null +++ b/src/components/ERC20_Pool.tsx @@ -0,0 +1,31 @@ +import React, { useEffect } from "react"; +import { getAllERC20 } from "src/api/client"; +import { setERC20Pool, Erc20 } from "src/hooks/ERC20_Pool"; + +export function ERC20_Pool() { + useEffect(() => { + const getRates = async () => { + const erc20: Erc20[] = await getAllERC20(); + const erc20Map = {} as Record; + erc20.forEach((i: any) => { + erc20Map[i.address] = i; + }); + + window.localStorage.setItem("ERC20_Pool", JSON.stringify(erc20Map)); + setERC20Pool(erc20Map); + }; + + let tId = 0; + + setTimeout(() => { + getRates(); + tId = window.setInterval(getRates, 10 * 60 * 1e3); + }); + + return () => { + clearTimeout(tId); + }; + }, []); + + return null; +} diff --git a/src/components/ERC721_Pool.tsx b/src/components/ERC721_Pool.tsx new file mode 100644 index 0000000..cab8a4d --- /dev/null +++ b/src/components/ERC721_Pool.tsx @@ -0,0 +1,32 @@ +import React, { useEffect } from "react"; +import { getAllERC721 } from "src/api/client"; +import { setERC721Pool, ERC721 } from "src/hooks/ERC721_Pool"; + +export function ERC721_Pool() { + useEffect(() => { + const getRates = async () => { + const erc721: ERC721[] = await getAllERC721(); + const erc721Map = {} as Record; + erc721.forEach((i: any) => { + erc721Map[i.address] = i; + }); + + window.localStorage.setItem("ERC721_Pool", JSON.stringify(erc721Map)); + setERC721Pool(erc721Map); + }; + + let tId = 0; + + setTimeout(() => { + getRates(); + // console.log("GET 721"); + tId = window.setInterval(getRates, 10 * 60 * 1e3); + }); + + return () => { + clearTimeout(tId); + }; + }, []); + + return null; +} diff --git a/src/components/ONE_USDT_Rate.tsx b/src/components/ONE_USDT_Rate.tsx new file mode 100644 index 0000000..3d53135 --- /dev/null +++ b/src/components/ONE_USDT_Rate.tsx @@ -0,0 +1,50 @@ +import React, { useEffect } from "react"; +import dayjs from "dayjs"; +export function ONE_USDT_Rate() { + useEffect(() => { + const getRates = () => { + const rates = {} as Record; + fetch("https://api.binance.com/api/v3/klines?symbol=ONEUSDT&interval=1d") + .then((_res) => _res.json()) + .then((res) => { + res.forEach((t: Array) => { + rates[String(t[0])] = Number(t[1]); + }); + window.localStorage.setItem('ONE_USDT_rates', JSON.stringify(rates)) + }); + }; + getRates(); + const tId = window.setInterval(getRates, 10 * 60 * 1e3); + + return () => { + clearTimeout(tId); + } + }, []); + return null; +} + +export function getNearestPriceForTimestamp(timestampString: string) { + const rates = JSON.parse(window.localStorage.getItem('ONE_USDT_rates') || '{}') as Record; + const timestamps = Object.keys(rates); + const prices = Object.values(rates); + const timestamp = dayjs(timestampString).valueOf(); + + if(timestamp >= +timestamps.slice(-1)[0]) { + return prices.slice(-1)[0]; + } + + if(timestamp <= +timestamps[0]) { + return -1; + } + + if(timestamps.length) { + let i = 0; + while(+timestamps[i] <= timestamp) { + i++; + } + + return prices[i]; + } + + return 0; +} diff --git a/src/components/appFooter/index.tsx b/src/components/appFooter/index.tsx new file mode 100644 index 0000000..35f80f9 --- /dev/null +++ b/src/components/appFooter/index.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import { Box, Text } from "grommet" +import { Group, Medium, Twitter } from 'grommet-icons' +import styled, {CSSProperties} from 'styled-components'; + +import { TelegramIcon, DiscordIcon } from 'src/components/ui/icons' + +const IconAhchor = styled.a` + opacity: 0.9; + transition: 0.17s ease all; + + &:hover { + opacity: 1; + } +`; + +export function AppFooter(props: { style: CSSProperties }) { + + return ( + + + + + + + + + + + + + + + + + + + + {/**/} + {/* Terms of Use*/} + {/* |*/} + {/* Privacy Policy*/} + {/**/} + + © + Harmony {new Date().getFullYear()} + . + hello@harmony.one + + + + ) +} \ No newline at end of file diff --git a/src/components/appHeader/ConfigureButton.tsx b/src/components/appHeader/ConfigureButton.tsx new file mode 100644 index 0000000..e17c3cd --- /dev/null +++ b/src/components/appHeader/ConfigureButton.tsx @@ -0,0 +1,117 @@ +import React from "react"; +import { Menu } from "grommet-icons"; +import { Box, Text, DropButton } from "grommet"; +import styled from "styled-components"; +import { + useThemeMode, + setThemeMode, + themeType, +} from "src/hooks/themeSwitcherHook"; + +import { + useCurrency, + setCurrency, + currencyType, +} from "src/hooks/ONE-ETH-SwitcherHook"; + +export function ConfigureButton() { + const theme = useThemeMode(); + const currency = useCurrency(); + + return ( + } + dropAlign={{ top: "bottom", right: "right" }} + style={{ + border: "none", + boxShadow: "none", + paddingRight: "6px", + paddingLeft: 0, + }} + dropContent={ + + + Theme + + + + Address style + + + + } + /> + ); +} + +interface ToggleProps { + value: string; + options: Array<{ + text: string; + value: themeType | currencyType; + }>; + onChange: (value: any) => void; +} + +//@ts-ignore +const ToggleButton = (props: ToggleProps) => { + const { options, value, onChange } = props; + + return ( + + {options.map((i) => ( + onChange(i.value)} + key={i.value} + > + {i.text} + + ))} + + ); +}; + +const SwitchButton = styled.div<{ selected: boolean }>` + padding: 8px 20px; + min-width: 60px; + background-color: ${(props) => + props.selected ? props.theme.global.colors.brand : "transparent"}; + color: ${(props) => + props.selected + ? props.theme.global.colors.background + : props.theme.global.colors.brand}; + font-weight: ${(props) => (props.selected ? "bold" : "normal")}; + user-select: none; + outline: none; + text-align: center; + cursor: ${(props) => (props.selected ? "auto" : "pointer")}; +`; diff --git a/src/components/appHeader/InfoButton.tsx b/src/components/appHeader/InfoButton.tsx new file mode 100644 index 0000000..ecc2d3e --- /dev/null +++ b/src/components/appHeader/InfoButton.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { CaretDownFill } from "grommet-icons"; +import { Box, DropButton, Anchor, Text } from "grommet"; +import { useHistory } from "react-router-dom"; + +export function InfoButton() { + const history = useHistory(); + + return ( + + + Tokens + + + + } + dropAlign={{ top: "bottom", right: "right" }} + dropContent={ + + { + e.preventDefault(); + history.push("/hrc20"); + }} + > + HRC20 tokens + + { + e.preventDefault(); + history.push("/hrc721"); + }} + > + HRC721 tokens + + { + e.preventDefault(); + history.push("/hrc1155"); + }} + > + HRC1155 tokens + + + } + style={{ + border: "none", + boxShadow: "none", + paddingRight: "6px", + paddingBottom: "8px", + }} + /> + ); +} diff --git a/src/components/appHeader/index.tsx b/src/components/appHeader/index.tsx new file mode 100644 index 0000000..58805cd --- /dev/null +++ b/src/components/appHeader/index.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import { Box, Heading, Text } from "grommet"; +import { FiatPrice, BaseContainer } from "src/components/ui"; +import { useHistory } from "react-router-dom"; +import { ConfigureButton } from "./ConfigureButton"; +import { InfoButton } from "./InfoButton"; +import { useThemeMode } from "src/hooks/themeSwitcherHook"; + +import styled, { CSSProperties } from "styled-components"; + +const HeaderLine = (props: any) => { + //@ts-ignore + const isDark = useThemeMode() === "dark"; + + return ( + + ); +}; + +const ProjectName = styled(Box)` + margin-left: 3px; +`; + +export function AppHeader(props: { style: CSSProperties }) { + const history = useHistory(); + + return ( + + + history.push("/")} + > + + + + Harmony Block Explorer{" "} + + beta + + + + + + + + + + + + ); +} diff --git a/src/components/block/BlockDetails.tsx b/src/components/block/BlockDetails.tsx new file mode 100644 index 0000000..8acb704 --- /dev/null +++ b/src/components/block/BlockDetails.tsx @@ -0,0 +1,121 @@ +import React, { FunctionComponent, useState } from "react"; +import { Block } from "../../types"; +import { + blockPropertyDisplayNames, + blockPropertySort, + blockPropertyDescriptions, + blockDisplayValues, +} from "./helpers"; +import { TipContent } from "src/components/ui"; +import { Box, DataTable, Tip, Anchor, Text } from "grommet"; + +import { CircleQuestion, CaretDownFill, CaretUpFill } from "grommet-icons"; + +const columns = [ + { + property: "key", + render: (e: any) => ( +
+ } + plain + > + + + + +  {blockPropertyDisplayNames[e.key] || e.key} +
+ ), + size: "1/3", + }, + { + property: "value", + size: "2/3", + render: (e: any) => e.value, + }, +]; + +type BlockDetailsProps = { + block: Block; + blockNumber: number; +}; +type tableEntry = { + key: string; + value: any; +}; + +export const BlockDetails: FunctionComponent = ({ + block, + blockNumber, +}) => { + const [showDetails, setShowDetails] = useState(true); + + const keys = Object.keys({ ...block, shard: blockNumber }); + const sortedKeys = keys.sort( + (a, b) => blockPropertySort[b] - blockPropertySort[a] + ); + // show 8 till gas used + const filteredKeys = sortedKeys.filter((k, i) => showDetails || i < 8); + const blockData = filteredKeys.reduce((arr, key) => { + // @ts-ignore + const value = + key === "shard" ? ( + {blockNumber} + ) : ( + blockDisplayValues(block, key, (block as any)[key]) + ); + + arr.push({ key, value } as tableEntry); + return arr; + }, [] as tableEntry[]); + + return ( + <> + + + + setShowDetails(!showDetails)} + margin={{ top: "medium" }} + > + {showDetails ? ( + <> + Show less  + + + ) : ( + <> + Show more  + + + )} + + + + + ); +}; diff --git a/src/components/block/BlockList.tsx b/src/components/block/BlockList.tsx new file mode 100644 index 0000000..7008e31 --- /dev/null +++ b/src/components/block/BlockList.tsx @@ -0,0 +1,94 @@ +import React, { FunctionComponent, useState } from 'react' +import { Block } from '../../types' +import { + blockPropertyDisplayNames, + blockPropertySort, + blockPropertyDescriptions, + blockDisplayValues +} from './helpers' +import { TipContent } from 'src/components/ui' +import { + Box, + DataTable, + Tip, + Anchor +} from 'grommet' + +import { CircleQuestion, CaretDownFill, CaretUpFill } from 'grommet-icons' + +const columns = [ + { + property: 'shardID', + }, + { + property: 'timestamp', + }, + { + property: 'transactions', + }, + { + property: 'miner' + }, + { + property: 'gasUsed', + }, + { + property: 'gasLimit' + } +] + +type BlockDetailsProps = { + block: Block +} +type tableEntry = { + key: string, + value: any +} + +export const BlockList: FunctionComponent = ({ block }) => { + const [showDetails, setShowDetails] = useState(true) + + const keys = Object.keys(block) + const sortedKeys = keys.sort((a, b) => blockPropertySort[b] - blockPropertySort[a]) + // show first 8 till gas used + const filteredKeys = sortedKeys.filter((k, i) => showDetails || i < 8) + const blockData = filteredKeys + .reduce((arr, key) => { + // @ts-ignore + const value = blockDisplayValues(block, key, block[key]) + arr.push({ key, value } as tableEntry) + return arr + }, [] as tableEntry[]) + + return <> + +
+ Block #{block.number} +
+ + setShowDetails(!showDetails)}> + {showDetails + ? <>Show less  + + : <>Show more  + + } + +
+ +} \ No newline at end of file diff --git a/src/components/block/helpers.tsx b/src/components/block/helpers.tsx new file mode 100644 index 0000000..8210833 --- /dev/null +++ b/src/components/block/helpers.tsx @@ -0,0 +1,241 @@ +import { + Address, + Timestamp, + BlockHash, + BlockNumber, + TransactionHash, + formatNumber, +} from "src/components/ui"; +import { + Clone, + FormPreviousLink, + FormNextLink, + StatusGood, +} from "grommet-icons"; +import { Link } from "react-router-dom"; + +import React from "react"; +import { Block } from "src/types"; +import { CopyBtn } from "../ui/CopyBtn"; +import { toaster } from "src/App"; +import { Box, Text } from "grommet"; +import styled from "styled-components"; + +const Icon = styled(StatusGood)` + margin-right: 5px; +`; + +export const blockPropertyDisplayNames: Record = { + number: "Height", + hash: "Hash", + miner: "Proposer", + extraData: "Extra Data", + gasLimit: "Gas Limit", + gasUsed: "Gas Used", + timestamp: "Timestamp", + difficulty: "Difficulty", + logsBloom: "Logs Bloom", + mixHash: "Mix Hash", + nonce: "Nonce", + parentHash: "Parent Hash", + receiptsRoot: "Receipts Root", + sha3Uncles: "SHA3 Uncles", + size: "Size", + stateRoot: "State Root", + transactions: "Transactions", + stakingTransactions: "Staking Transactions", + transactionsRoot: "Transactions Root", + uncles: "Uncles", + epoch: "Epoch", + viewID: "View ID", + shard: "Shard", +}; + +export const blockPropertyDescriptions: Record = { + number: + "Also known as Block Number. The block height, which indicates the length of the blockchain, increases after the addition of the new block.", + hash: "The hash of the block header of the current block.", + miner: "Miner who successfully include the block onto the blockchain.", + extraData: "Any data that can be included by the miner in the block.", + gasLimit: "Total gas limit provided by all transactions in the block.", + gasUsed: + "The total gas used in the block and its percentage of gas filled in the block.", + timestamp: "The date and time at which a block is mined.", + difficulty: + "The amount of effort required to mine a new block. The difficulty algorithm may adjust according to time.", + logsBloom: "Logs Bloom", + mixHash: "Mix Hash", + nonce: + "Block nonce is a value used during mining to demonstrate proof of work for a block.", + parentHash: + "The hash of the block from which this block was generated, also known as its parent block.", + receiptsRoot: "Receipts Root", + sha3Uncles: + "The mechanism which Ethereum Javascript RLP encodes an empty string.", + size: "The block size is actually determined by the block's gas limit.", + stateRoot: "The root of the state trie", + transactions: + "The number of transactions in the block. Internal transaction is transactions as a result of contract execution that involves ONE value.", + stakingTransactions: "The number of staking transactions in the block.", + transactionsRoot: "Transactions Root", + uncles: "Uncles", + epoch: "Epoch", + viewID: "View ID", +}; + +export const blockPropertySort: Record = { + number: 1000, + shard: 997, + hash: 995, + miner: 960, + extraData: 500, + gasLimit: 900, + gasUsed: 890, + timestamp: 990, + difficulty: 500, + logsBloom: 500, + mixHash: 500, + nonce: 500, + parentHash: 500, + receiptsRoot: 500, + sha3Uncles: 500, + size: 700, + stateRoot: 500, + transactions: 980, + stakingTransactions: 970, + transactionsRoot: 500, + uncles: 500, + epoch: 500, + viewID: 500, +}; +const emptyLogBloom = + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; +const emptyMixHash = + "0x0000000000000000000000000000000000000000000000000000000000000000"; + +export const blockPropertyDisplayValues: any = { + // @ts-ignore + number: (value: any) => ( + <> + +   + {value > 0 && ( + + + + )} + + + + + ), + transactions: (value: any[]) => + value.length > 0 + ? value.map((tx) => ( + <> + + toaster.show({ + message: () => ( + + + Copied to clipboard + + ), + }) + } + /> +   + +
+ + )) + : null, + stakingTransactions: (value: any[]) => + value.length > 0 + ? value.map((tx) => ( + <> + + toaster.show({ + message: () => ( + + + Copied to clipboard + + ), + }) + } + /> +   + +
+ + )) + : null, + miner: (value: any) =>
, + hash: (value: any) => , + parentHash: (value: any) => , + timestamp: (value: any) => , + gasUsed: (value: any, block: Block) => ( + + {formatNumber(+value)} ({+value / +block.gasLimit}%){" "} + + ), + gasLimit: (value: any) => <>{formatNumber(+value)}, + size: (value: any) => <>{formatNumber(+value)}, +}; + +export const blockDisplayValues = (block: Block, key: string, value: any) => { + const f = blockPropertyDisplayValues[key]; + + let displayValue = value; + + if (f) { + displayValue = f(value, block); + } else { + if (Array.isArray(value)) { + displayValue = value.join(", "); + } + + if (displayValue === emptyLogBloom || displayValue === emptyMixHash) { + displayValue = null; + } else if (value && value.length && value.length > 66) { + displayValue = value.slice(0, 63) + "..."; + } + + if (displayValue === "0x") { + displayValue = null; + } + } + + return ( +
+ {!["transactions", "stakingTransactions", "uncles", "nonce"].includes( + key + ) && + !["0x", "0", 0, null].includes(displayValue) && + !["miner"].find((item) => item === key) && ( + <> + + toaster.show({ + message: () => ( + + + Copied to clipboard + + ), + }) + } + /> +   + + )} + {displayValue || "—"} +
+ ); +}; diff --git a/src/components/dropdown/Dropdown.tsx b/src/components/dropdown/Dropdown.tsx new file mode 100644 index 0000000..04426fc --- /dev/null +++ b/src/components/dropdown/Dropdown.tsx @@ -0,0 +1,227 @@ +import { Box, TextInput } from "grommet"; +import { CaretDownFill, CaretUpFill, Search } from "grommet-icons"; +import React, { Fragment } from "react"; +import styled, { css } from "styled-components"; + +export interface IDropdownProps { + defaultValue?: T; + value?: T; + className?: string; + keyField: keyof T; + renderValue: (dataItem: T) => JSX.Element; + renderItem: (dataItem: T) => JSX.Element; + items: T[]; + isOpen?: boolean; + searchable?: boolean | ((dataItem: T, searchText: string) => boolean); + group?: { + groupBy: keyof T; + renderGroupItem: () => JSX.Element; + }[]; + onToggle?: (isOpen: boolean) => void; + onClickItem?: (dataItem: T) => void; + themeMode: "dark" | "light"; + itemHeight: string; + itemStyles: React.CSSProperties; +} + +const DropdownWrapper = styled(Box)` + width: 100%; + height: 37px; + padding: 5px; + border-radius: 8px; + margin: 5px; + position: relative; + user-select: none; +`; + +const Value = styled(Box)` + width: 100%; + cursor: pointer; +`; + +const DataList = styled(Box)` + position: absolute; + max-height: 300px; + overflow: auto; + width: 100%; + top: 38px; + border-radius: 8px; + left: 0px; + z-index: 1; +`; + +const DataItem = styled(Box)<{ + itemHeight: string; +}>` + cursor: pointer; + + ${(props) => { + return css` + min-height: ${props.itemHeight}; + `; + }} +`; + +export class Dropdown extends React.Component< + IDropdownProps, + { isOpen: boolean; searchText: string } +> { + public element!: HTMLDivElement; + + public initValue: T = this.props.defaultValue || this.props.items[0]; + + private get selectedValue() { + return this.props.value || this.initValue; + } + + public state = { + isOpen: this.props.isOpen || false, + searchText: "", + }; + + componentDidMount() { + document.body.addEventListener("click", this.handleClickBody as any); + } + + componentWillUnmount() { + document.body.removeEventListener("click", this.handleClickBody as any); + } + + handleClickBody = (e: React.MouseEvent) => { + if (!(this.element && this.element.contains(e.target as Node))) { + this.setState({ ...this.state, isOpen: false }); + } + }; + + onClickItem = (item: T, evt: React.MouseEvent) => { + this.initValue = item; + + if (this.props.onClickItem) { + this.props.onClickItem(item); + } + + this.setState({ ...this.state, isOpen: false }); + }; + + renderGroupItems() { + const { + group = [], + searchable, + itemHeight = "47px", + itemStyles = {}, + } = this.props; + + return group.map((groupItem) => { + const items = this.props.items + .filter((item) => item[groupItem.groupBy]) + .filter((item) => + searchable + ? typeof searchable === "function" + ? searchable(item, this.state.searchText) + : true // TODO hardcode + : true + ); + + return items.length ? ( + + {groupItem.renderGroupItem()} + {items.map((item) => ( + this.onClickItem(item, evt)} + itemHeight={itemHeight} + style={{ ...itemStyles }} + > + {this.props.renderItem(item)} + + ))} + + ) : null; + }); + } + + render() { + const { + group = [], + searchable, + themeMode, + itemHeight = "47px", + itemStyles = {}, + } = this.props; + + return ( + (this.element = element as HTMLDivElement)} + border={{ size: "xsmall", color: "border" }} + > + { + this.setState({ ...this.state, isOpen: !this.state.isOpen }); + }} + direction={"row"} + flex + > + {this.props.renderValue(this.selectedValue)} + {this.state.isOpen ? ( + { + console.log('CLICK') + e.stopPropagation() + this.setState({ ...this.state, isOpen: false }); + }} + /> + ) : ( + { + e.stopPropagation() + this.setState({ ...this.state, isOpen: true }); + }} + /> + )} + + {this.state.isOpen ? ( + + {searchable ? ( + ) => { + this.setState({ + ...this.state, + searchText: evt.currentTarget.value, + }); + }} + color="red" + icon={} + style={{ + backgroundColor: + themeMode === "light" ? "white" : "transparent", + fontWeight: 500, + }} + placeholder="Search by symbol, token address" + /> + ) : null} + {group.length + ? this.renderGroupItems() + : this.props.items.map((item) => ( + this.onClickItem(item, evt)} + itemHeight={itemHeight} + style={{ ...itemStyles }} + > + {this.props.renderItem(item)} + + ))} + + ) : null} + + ); + } +} diff --git a/src/components/metrics/index.tsx b/src/components/metrics/index.tsx new file mode 100644 index 0000000..8e9395c --- /dev/null +++ b/src/components/metrics/index.tsx @@ -0,0 +1,297 @@ +import React, { useEffect, useState } from "react"; +import { Box, Text, DataChart, Spinner } from "grommet"; +import { BasePage } from "src/components/ui"; +import { formatNumber } from "src/components/ui/utils"; +import { LatencyIcon } from "src/components/ui/icons"; +import dayjs from "dayjs"; +import { Transaction, LineChart, Cubes } from "grommet-icons"; +import styled from "styled-components"; +import { useMediaQuery } from "react-responsive"; +import { breakpoints } from "src/Responive/breakpoints"; +import { useONEExchangeRate } from "../../hooks/useONEExchangeRate"; +import { getTransactionCountLast14Days } from "src/api/client"; + +import { getCount } from "src/api/client"; + +export const Metrics = (params: { latency: number }) => { + const isLessLaptop = useMediaQuery({ maxDeviceWidth: "852px" }); + const isLessTablet = useMediaQuery({ maxDeviceWidth: breakpoints.tablet }); + const isLessMobileM = useMediaQuery({ maxDeviceWidth: "468px" }); + + return ( + + + + {!isLessMobileM && } + + + + + {!isLessMobileM && } + + + {isLessLaptop && ( + + )} + + + + + ); +}; + +function ONEPrice() { + const { lastPrice = 0, priceChangePercent = 0 } = useONEExchangeRate(); + + return ( + + + + + + + {"ONE PRICE"} + + + + $ {(+lastPrice).toFixed(2)} + + 0 ? "status-ok" : "#d23540"} + > + ({priceChangePercent > 0 ? "+" : ""} + {formatNumber(priceChangePercent)}%) + + + + + ); +} + +function TransactionsCount() { + const [count, setCount] = useState(""); + + useEffect(() => { + let tId = 0; + const getRes = async () => { + try { + let res = await getCount([0, "transactions"]); + setCount(res.count); + } catch (err) { + console.log(err); + } + }; + getRes(); + tId = window.setInterval(getRes, 30000); + + return () => { + clearTimeout(tId); + }; + }, []); + + return ( + + + + + + + {"TRANSACTIONS COUNT"} + + + {formatNumber(+count)} + + + + ); +} + +function ShardCount() { + const count = process.env.REACT_APP_AVAILABLE_SHARDS?.split(",").length || 0; + + return ( + + + + + + + {"SHARD COUNT"} + + + {formatNumber(count)} + + + + ); +} + +function BlockLatency(params: { latency: number }) { + return ( + + + + + + + {"BLOCK LATENCY"} + + + {params.latency.toFixed(2)}s + + + + ); +} + +interface TxHitoryItem { + timestamp: string; + count: string; +} + +function BlockTransactionsHistory() { + const [result, setResult] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + const getElements = async () => { + setIsLoading(true); + const res = await getTransactionCountLast14Days(); + setResult(res); + setIsLoading(false); + }; + + getElements(); + }, []); + + const data = result.map((i) => ({ + date: dayjs(i.timestamp).format("DD-MM"), + count: +i.count, + })); + + return ( + + + {"TRANSACTION HISTORY"} + + + {isLoading && ( + + + + )} + {!isLoading && ( + ( + + {value} + + ), + }, + { + property: "count", + label: "Transactions", + render: (value) => ( + + {formatNumber(value)} + + ), + }, + ]} + size="fill" + chart={[ + { + property: "count", + type: "bar", + color: "brand", + opacity: "medium", + thickness: "small", + }, + ]} + /> + )} + + + ); +} + +const Line = styled.div<{ horizontal?: boolean; vertical?: boolean }>` + display: flex; + width: ${(props) => (props.horizontal ? "100%" : "1px")}; + height: ${(props) => (props.vertical && !props.horizontal ? "100%" : "1px")}; + background-color: ${(props) => props.theme.global.colors.border}; +`; diff --git a/src/components/pagination/Pagination.tsx b/src/components/pagination/Pagination.tsx new file mode 100644 index 0000000..d215761 --- /dev/null +++ b/src/components/pagination/Pagination.tsx @@ -0,0 +1,34 @@ +import { Box } from "grommet"; +import { FormNext, FormPrevious } from "grommet-icons"; +import React, { useState } from "react"; + +export interface IPaginationProps { + onClickNext: () => void; + onClickPrev: () => void; + disablePrev?: boolean; + disableNext?: boolean; +} + +export function Pagination(props: IPaginationProps) { + return ( + + props.onClickPrev()} + /> + props.onClickNext()} + /> + + ); +} diff --git a/src/components/tables/TableComponents.tsx b/src/components/tables/TableComponents.tsx new file mode 100644 index 0000000..9e2b191 --- /dev/null +++ b/src/components/tables/TableComponents.tsx @@ -0,0 +1,85 @@ +import { DataTable, DataTableExtendedProps } from "grommet"; +import React from "react"; +import styled from "styled-components"; + +export interface ITableComponentProps { + tableProps: DataTableExtendedProps; + className?: string; + alwaysOpenedRowDetails?: boolean; +} + +const Flex = styled.div` + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + box-sizing: border-box; + max-width: 100%; + min-width: 0; + min-height: 0; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; +`; + +export class TableComponent extends React.Component { + private element!: HTMLDivElement; + + componentDidUpdate() { + this.clickExpandButtons(); + } + + clickExpandButtons = () => { + if (this.props.alwaysOpenedRowDetails) { + const headerTd = Array.from( + this.element.querySelectorAll("table thead tr td:first-child") + ) as HTMLButtonElement[]; + + headerTd.forEach((td) => (td.style.display = "none")); + + const bodyTd = Array.from( + this.element.querySelectorAll("table tbody tr td:first-child") + ) as HTMLButtonElement[]; + + bodyTd.forEach((td) => (td.style.display = "none")); + + const expandButtons = Array.from( + this.element.querySelectorAll("table tr td:first-child button") + ) as HTMLButtonElement[]; + + expandButtons.forEach((expandButton) => { + const svg = expandButton.querySelector("svg") as SVGElement; + (svg.parentNode as HTMLDivElement).style.display = "none"; + expandButton.style.width = "1px"; + expandButton.style.height = "1px"; + }); + + setTimeout(() => { + expandButtons.forEach((item) => item.click()); + }, 10); + } + }; + + render() { + return ( + (this.element = element)} + className={this.props.className} + > + { + if (this.props.tableProps.onMore) { + this.props.tableProps.onMore(); + } + this.clickExpandButtons(); + } + : undefined + } + /> + + ); + } +} diff --git a/src/components/tables/TransactionsTable.tsx b/src/components/tables/TransactionsTable.tsx new file mode 100644 index 0000000..fdd1c4d --- /dev/null +++ b/src/components/tables/TransactionsTable.tsx @@ -0,0 +1,286 @@ +import React, { useEffect, useRef, useState } from "react"; + +import { Box, DataTable, Text, Spinner, ColumnConfig } from "grommet"; +import { Filter, RPCTransactionHarmony } from "src/types"; +import { useHistory } from "react-router-dom"; +import { FormNextLink } from "grommet-icons"; +import { + Address, + formatNumber, + RelativeTimer, + PaginationNavigator, + PaginationRecordsPerPage, + ONEValue, +} from "src/components/ui"; +import { TableComponent } from "./TableComponents"; + +function getColumns(props: any) { + const { history } = props; + return [ + { + property: "shard", + size: "xxsmall", + resizeable: false, + header: ( + + Shard + + ), + render: (data: RPCTransactionHarmony) => ( + + {data.shardID} + + {data.toShardID} + + ), + }, + { + property: "hash", + size: "xsmall", + resizeable: false, + header: ( + + Hash + + ), + render: (data: RPCTransactionHarmony) => ( + { + history.push(`/tx/${data.hash}`); + }} + color="brand" + > +
+ + ), + }, + { + property: "block_number", + size: "260px", + resizeable: false, + header: ( + + Block number + + ), + render: (data: RPCTransactionHarmony) => { + return ( + { + history.push(`/block/${data.blockNumber}`); + }} + color="brand" + > + {formatNumber(+data.blockNumber)} + + ); + }, + }, + { + property: "from", + size: "large", + resizeable: false, + header: ( + + From + + ), + render: (data: RPCTransactionHarmony) =>
, + }, + { + property: "to", + size: "large", + resizeable: false, + header: ( + + To + + ), + render: (data: RPCTransactionHarmony) =>
, + }, + { + property: "value", + size: "380px", + resizeable: false, + header: ( + + ONEValue + + ), + render: (data: RPCTransactionHarmony) => ( + + + + ), + }, + { + property: "timestamp", + size: "280px", + resizeable: false, + header: ( + + Timestamp + + ), + render: (data: RPCTransactionHarmony) => ( + + {/**/} + {/* {dayjs(data.timestamp).format("YYYY-MM-DD, HH:mm:ss")},*/} + {/**/} + + + ), + }, + ]; +} + +interface TransactionTableProps { + rowDetails?: (row: any) => JSX.Element; + data: any[]; + columns?: ColumnConfig[]; + totalElements: number; + limit: number; + filter: Filter; + setFilter: (filter: Filter) => void; + showIfEmpty?: boolean; + emptyText?: string; + hidePagination?: boolean; + isLoading?: boolean; + hideCounter?: boolean; + minWidth?: string; + noScrollTop?: boolean; + step?: number; + primaryKey?: string; +} + +export function TransactionsTable(props: TransactionTableProps) { + const history = useHistory(); + const { + data, + totalElements, + limit, + step = 10, + filter, + setFilter, + showIfEmpty, + emptyText = "No data to display", + columns, + hidePagination, + isLoading, + hideCounter, + noScrollTop, + minWidth = "1310px", + } = props; + + const _IsLoading = isLoading; + + return ( + <> + + {!hideCounter ? ( + + {Math.min(limit, data.length)} transaction + {data.length !== 1 ? "s" : ""} shown + + ) : ( + + )} + {!hidePagination && ( + + )} + + + {_IsLoading ? ( + + + + ) : !data.length && !_IsLoading ? ( + + {emptyText} + + ) : ( + ( +
+ {props.rowDetails && props.rowDetails(row)} +
+ ) + : undefined, + }} + /> + )} +
+ {!hidePagination && ( + + + + + )} + + ); +} diff --git a/src/components/transaction/InternalTransactionList.tsx b/src/components/transaction/InternalTransactionList.tsx new file mode 100644 index 0000000..e064c7a --- /dev/null +++ b/src/components/transaction/InternalTransactionList.tsx @@ -0,0 +1,142 @@ +import React, { useState } from "react"; +import { Box, Text } from "grommet"; + +import { TransactionsTable } from "src/components/tables/TransactionsTable"; +import { Filter, InternalTransaction } from "src/types"; +import { + Address, + ONEValue, + PaginationNavigator, + TransactionType, +} from "src/components/ui"; +import { DisplaySignatureMethod } from "src/web3/parseByteCode"; + +interface InternalTransactionListProps { + list: InternalTransaction[]; + hash: string; + timestamp: string; +} + +const initFilter: Filter = { + offset: 0, + limit: 10, + orderBy: "block_number", + orderDirection: "desc", + filters: [{ type: "gte", property: "block_number", value: 0 }], +}; + +export function InternalTransactionList(props: InternalTransactionListProps) { + const { list, hash, timestamp } = props; + const [filter, setFilter] = useState(initFilter); + + const { limit = 10, offset = 0 } = filter; + const pageSize = 10; + const curPage = +(+offset / limit).toFixed(0) + 1; + + const data = list + .sort((a, b) => (a.index > b.index ? 1 : -1)) + .slice(pageSize * (curPage - 1), pageSize * curPage) + .map((item) => ({ ...item })); + + return ( + + (a.index > b.index ? 1 : -1))} + totalElements={data.length} + step={data.length} + showIfEmpty + emptyText={"No Internal Transactions for this hash " + hash} + limit={+limit} + filter={filter} + setFilter={setFilter} + minWidth="960px" + primaryKey={"index"} + rowDetails={(row) => ( + + )} + /> + + ); +} + +function getColumns(props?: any) { + const { timestamp } = props; + + return [ + { + property: "type", + header: ( + + Type + + ), + render: (data: InternalTransaction) => ( + + + + ), + }, + /* { + property: "method", + header: ( + + Suggested Method + + ), + render: (data: InternalTransaction) => { + let signature; + try { + // @ts-ignore + signature = + data.signatures && + data.signatures.map((s) => s.signature)[0].split("(")[0]; + } catch (err) {} + + return {signature || "—"}; + }, + },*/ + { + property: "from", + header: ( + + From + + ), + render: (data: InternalTransaction) => ( + +
+ + ), + }, + { + property: "to", + header: ( + + To + + ), + render: (data: InternalTransaction) => ( + +
+ + ), + }, + { + property: "value", + header: ( + + ONEValue + + ), + render: (data: InternalTransaction) => ( + + + + ), + }, + ]; +} diff --git a/src/components/transaction/TransactionDetails.tsx b/src/components/transaction/TransactionDetails.tsx new file mode 100644 index 0000000..bc003ab --- /dev/null +++ b/src/components/transaction/TransactionDetails.tsx @@ -0,0 +1,213 @@ +import React, { FunctionComponent, useState } from "react"; +import { Log, RPCStakingTransactionHarmony } from "src/types"; + +import { + transactionPropertyDisplayNames, + transactionDisplayValues, + transactionPropertySort, + transactionPropertyDescriptions, +} from "./helpers"; +import { Address, CalculateFee, TipContent } from "src/components/ui"; +import { Anchor, Box, DataTable, Text, Tip } from "grommet"; +import { TransactionSubType } from "src/components/transaction/helpers"; +import { parseSuggestedEvent, DisplaySignature } from "src/web3/parseByteCode"; + +import { CaretDownFill, CaretUpFill, CircleQuestion } from "grommet-icons"; +import { ERC20Value } from "../ERC20Value"; +import { TokenValueBalanced } from "../ui/TokenValueBalanced"; +import { TxStatusComponent } from "../ui/TxStatusComponent"; + +const getColumns = ({ type = "" }) => [ + { + property: "key", + render: (e: any) => ( +
+ + } + plain + > + + + + +   + {transactionPropertyDisplayNames[e.key + type] || + transactionPropertyDisplayNames[e.key] || + e.key} +
+ ), + size: "1/3", + }, + { + property: "value", + size: "2/3", + render: (e: any) => e.value, + }, +]; + +type TransactionDetailsProps = { + transaction: RPCStakingTransactionHarmony; + type?: TransactionSubType; + logs?: Log[]; + errorMsg: string | undefined; +}; + +type tableEntry = { + key: string; + value: any; +}; + +// todo move out to a service to support any custom ABI +const erc20TransferTopic = + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; + +const tokenTransfers = (logs: Log[]) => { + const erc20Logs = logs.filter((l) => l.topics.includes(erc20TransferTopic)); + const events = erc20Logs + .map((l) => + parseSuggestedEvent("Transfer(address,address,uint256)", l.data, l.topics) + ) + .filter((e) => e && e.parsed); + + if (!events.length) { + return <>—; + } + + return ( + <> + {events.map((e: any, index) => { + const val = e.parsed["$2"]; + const address = erc20Logs[index].address; + + return ( + + + + From :  + +
+   + + To :  + +
+ + + + Value :   + + + + + ); + })} + + ); +}; + +export const TransactionDetails: FunctionComponent = ({ + transaction, + type, + logs = [], + errorMsg, +}) => { + const [showDetails, setShowDetails] = useState(false); + + const newTransaction = { + Status: + errorMsg === undefined ? ( + +transaction.shardID > 0 ? ( + + ) : ( + <> + ) + ) : ( + + ), + ...transaction, + tokenTransfers: tokenTransfers(logs), + gasPrice: {CalculateFee(transaction)}, + }; + const keys = Object.keys(newTransaction); + const sortedKeys = keys + .sort((a, b) => transactionPropertySort[b] - transactionPropertySort[a]) + .filter((k) => showDetails || ["r", "s", "v"].indexOf(k) === -1); + + const txData = sortedKeys.reduce((arr, key) => { + // @ts-ignore + const value = transactionDisplayValues( + // @ts-ignore + newTransaction, + key, + // @ts-ignore + newTransaction[key], + type + ); + + if (value === undefined) { + return arr; + } + + arr.push({ key, value } as tableEntry); + return arr; + }, [] as tableEntry[]); + + return ( + <> + + + + + setShowDetails(!showDetails)} + margin={{ top: "medium" }} + > + {showDetails ? ( + <> + Show less  + + + ) : ( + <> + Show more  + + + )} + + + + ); +}; diff --git a/src/components/transaction/TransactionList.tsx b/src/components/transaction/TransactionList.tsx new file mode 100644 index 0000000..e7191dc --- /dev/null +++ b/src/components/transaction/TransactionList.tsx @@ -0,0 +1 @@ +export const todo = () => {} \ No newline at end of file diff --git a/src/components/transaction/TransactionLogs.tsx b/src/components/transaction/TransactionLogs.tsx new file mode 100644 index 0000000..71c8725 --- /dev/null +++ b/src/components/transaction/TransactionLogs.tsx @@ -0,0 +1,107 @@ +import React from 'react' +import { Box, Text } from 'grommet' + +import { Address } from 'src/components/ui' +import { parseSuggestedEvent, DisplaySignature } from 'src/web3/parseByteCode' + +interface TransactionLogsProps { + logs: any[]; + hash: string; +} + +export function TransactionLogs(props: TransactionLogsProps) { + const { logs, hash } = props + + if (!logs.length) { + return ( + + + No Logs for {hash} + + + ) + } + + return ( + + {logs + .sort((a, b) => a.logIndex - b.logIndex) + .map((log, i) => ( + + ))} + + ) +} + +interface LogItemProps { + log: { + address: string; + topics: string[]; + data: string; + signatures: any[] | null + }; +} + +const LogItem = (props: LogItemProps) => { + const { address, topics, data, signatures } = props.log + + let parsedEvents: any = null + + try { + // @ts-ignore + parsedEvents = signatures.map(s => s.signature) + .map(s => parseSuggestedEvent(s, data, topics)) + } catch (err) { + } + + const displaySignature = parsedEvents && parsedEvents[0] && DisplaySignature(parsedEvents[0]) || null + + return ( + + + + Address + + +
+ + + + {signatures && signatures.length ? + + + Suggested Event + + + {displaySignature || signatures[0].signature || ''} + + + : null} + + + + Topics + + + {topics.map(((topic, i) => ( + + {topic}{i !== topics.length - 1 ? ', ' : ''} + + )))} + + + + + Data + + + {data} + + + + ) +} diff --git a/src/components/transaction/helpers.tsx b/src/components/transaction/helpers.tsx new file mode 100644 index 0000000..1fac5cb --- /dev/null +++ b/src/components/transaction/helpers.tsx @@ -0,0 +1,279 @@ +import { Block, RPCTransactionHarmony } from "../../types"; +import { + Clone, + FormNextLink, + FormPreviousLink, + StatusGood, +} from "grommet-icons"; +import React from "react"; +import { blockPropertyDisplayValues } from "../block/helpers"; +import { + Address, + BlockHash, + BlockNumber, + Timestamp, + TransactionHash, + ONEValue, + StakingTransactionTypeValue, + CalculateFee, + formatNumber, +} from "../ui"; +import { Box, Text } from "grommet"; +import { CopyBtn } from "../ui/CopyBtn"; +import { toaster } from "src/App"; +import styled from "styled-components"; + +export const todo = {}; + +const Icon = styled(StatusGood)` + margin-right: 5px; +`; + +export type TransactionSubType = + | "__staking" + | "__delegated" + | "__undelegated" + | ""; + +export const transactionPropertyDisplayNames: Record = { + shardID: "Shard ID", + hash: "Ethereum Hash", + hash__staking: "Hash", + hash_harmony: "Hash", + value: "Value", + blockNumber: "Block Number", + from: "From", + txnFee: "Txn fee", + gas: "Gas", + gasPrice: "Gas Price", + input: "Input", + nonce: "Nonce", + r: "r", + s: "s", + timestamp: "Timestamp", + to: "To", + toShardID: "To Shard ID", + transactionIndex: "Transaction Index", + v: "v", + type: "Type", + amount: "Amount", + tokenTransfers: "Token Transfers", + name: "Name", + commissionRate: "Commission Rate", + maxCommissionRate: "Max Commission Rate", + maxChangeRate: "Max Change Rate", + minSelfDelegation: "Min Self Delegation", + maxTotalDelegation: "Max Total Delegation", + website: "Website", + identity: "Identity", + securityContract: "Security Contract", + details: "Details", + slotPubKeys: "Details", + + slotPubKeyToAdd: "Slot Pub Key To Add", + slotPubKeyToRemove: "Slot Pub Key To Remove", + + delegatorAddress: "Delegator Address", + validatorAddress: "Validator Address", +}; + +export const transactionPropertySort: Record = { + shardID: 1000, + hash: 900, + hash_harmony: 950, + value: 600, + tokenTransfers: 599, + blockNumber: 800, + blockHash: 799, + + from: 700, + to: 650, + txnFee: 560, + gas: 550, + gasPrice: 500, + input: 300, + nonce: 350, + r: 0, + s: 0, + timestamp: 750, + toShardID: 1, + transactionIndex: 350, + v: 0, +}; + +export const transactionPropertyDescriptions: Record = { + shardID: "The shard number where the transaction belongs.", + blockNumber: "The number of the block in which the transaction was recorded.", + hash: + "A TxHash or transaction hash is a unique 66 characters identifier that is generated whenever a transaction is executed.", + hash_harmony: + "A TxHash or transaction hash is a unique 66 characters identifier that is generated whenever a transaction is executed. Shard ID is also involved in calculation of Harmony Hash.", + from: + "The sending party of the transaction (could be from a contract address).", + to: "The receiving party of the transaction (could be a contract address).", + value: "The value being transacted in ONE and fiat value.", + txnFee: "Transaction fee", + gas: "The exact units of gas that was used for the transaction.", + transactionIndex: "Transaction's number in the block", + gasUsed: "The exact units of gas that was used for the transaction.", + gasPrice: + "Cost per unit of gas specified for the transaction, in ONE. The higher the gas price the higher chance of getting included in a block.", + input: "Additional information that is required for the transaction.", + gasLimit: "Total gas limit provided by all transactions in the block.", + timestamp: "The date and time at which a transaction is mined.", + difficulty: + "The amount of effort required to mine a new block. The difficulty algorithm may adjust according to time.", + nonce: + "Sequential running number for an address, beginning with 0 for the first transaction. For example, if the nonce of a transaction is 10, it would be the 11th transaction sent from the sender's address", + size: "The block size is actually determined by the block's gas limit.", + v: "Value for the transaction's signature", + r: "Value for the transaction's signature", + s: "Value for the transaction's signature", + validatorAddress: "Validator address", + validatorAddress__delegated: "Delegation validator address", + validatorAddress__undelegated: "Delegation delegator address", + delegatorAddress: "Delegator address", + delegatorAddress__delegated: "Delegator address", + delegatorAddress__undelegated: "Undelegation delegator address", + amount: "Stake amount for validator", + amount__delegated: "Amount for delegation to validator", + amount__undelegated: "Amount for undelegation to delegator", + name: "Validator name", + commissionRate: "Validator commission rate", + maxCommissionRate: "Validator commission rate", + maxChangeRate: "validator max commission rate change", + minSelfDelegation: "Min how much validator self delegates", + maxTotalDelegation: "Max total delegation to validator", + website: "Validator website", + identity: "Validator kyc identity", + securityContact: "Validator security contact", + details: "Additional validator info", + slotPubKeys: "Validator bls pub keys", + slotPubKeyToAdd: "Validator bls pub key to add", + slotPubKeyToRemove: "Validator bls pub key to remove", + tokenTransfers: "Token Transfers", +}; + +export const transactionPropertyDisplayValues: any = { + // @ts-ignore + blockNumber: (value: any,data: any) => , + from: (value: any) =>
, + value: (value: any, tx: any) => ( + + ), + to: (value: any) =>
, + hash: (value: any) => , + hash__staking: (value: any) => ( + + ), + hash_harmony: (value: any) => , + blockHash: (value: any) => , + timestamp: (value: any) => , + gasUsed: (value: any, tx: RPCTransactionHarmony) => ( + + {value} ({+value / +tx.gas}%){" "} + + ), + shardID: (value: any, tx: RPCTransactionHarmony) => ( + + {value} + + {tx.toShardID} + + ), + type: (value: any) => , + amount: (value: any, tx: any) => ( + + ), + + name: (value: any) => {value}, + delegatorAddress: (value: any) =>
, + validatorAddress: (value: any) =>
, + commissionRate: (value: any) => {value}, + maxCommissionRate: (value: any) => {value}, + maxChangeRate: (value: any) => {value}, + minSelfDelegation: (value: any) => {value}, + maxTotalDelegation: (value: any) => {value}, + website: (value: any) => {value}, + identity: (value: any) => {value}, + securityContact: (value: any) => {value}, + details: (value: any) => {value}, + slotPubKeys: (value: any) => {value}, + slotPubKeyToAdd: (value: any) => {value}, + slotPubKeyToRemove: (value: any) => {value}, + tokenTransfers: (value: any) => {value}, + gas: (value: any) => <>{formatNumber(+value)}, +}; + +export const transactionDisplayValues = ( + transaction: RPCTransactionHarmony, + key: string, + value: any, + type: string +) => { + if (["blockHash", "toShardID", "msg"].includes(key)) { + return; + } + + const f: null | Function = + transactionPropertyDisplayValues[key + type] || + transactionPropertyDisplayValues[key]; + + let displayValue = value; + + if (f) { + displayValue = f(value, transaction); + } else { + if (Array.isArray(value)) { + displayValue = value.join(", "); + } + + if (value && value.length && value.length > 66) { + displayValue = value.slice(0, 63) + "..."; + } + + if (displayValue === "0x") { + displayValue = null; + } + } + + if (displayValue === null || displayValue === undefined) { + if (["success", "error"].find((nameKey) => nameKey === key)) { + return; + } + + displayValue = "—"; + } + + const text = typeof value === "string" ? value : <>{value}; + const copyText = + typeof text === "string" && !["from", "to"].find((item) => item === key) + ? text + : ""; + + return ( + + {!["shardID"].includes(key) && ![0, "0", "—"].includes(displayValue) && ( + <> + {copyText ? ( + + toaster.show({ + message: () => ( + + + Copied to clipboard + + ), + }) + } + /> + ) : null} +   + + )} + {displayValue} + + ); +}; diff --git a/src/components/ui/Address.tsx b/src/components/ui/Address.tsx new file mode 100644 index 0000000..c8ebda6 --- /dev/null +++ b/src/components/ui/Address.tsx @@ -0,0 +1,125 @@ +import React, { CSSProperties } from "react"; +import { Box, Text } from "grommet"; +import { useHistory } from "react-router-dom"; +import { useERC20Pool } from "src/hooks/ERC20_Pool"; +import { getAddress } from "src/utils"; +import { useCurrency } from "src/hooks/ONE-ETH-SwitcherHook"; +import { useERC721Pool } from "src/hooks/ERC721_Pool"; +import { binanceAddressMap } from "src/config/BinanceAddressMap"; +import { useERC1155Pool } from "src/hooks/ERC1155_Pool"; +import { CopyBtn } from "./CopyBtn"; +import { toaster } from "src/App"; +import styled from "styled-components"; +import { StatusGood } from "grommet-icons"; + +const Icon = styled(StatusGood)` + margin-right: 5px; +`; + +interface IAddress { + address: string; + isShort?: boolean; + type?: "tx" | "address" | "staking-tx"; + style?: CSSProperties; + color?: string; + displayHash?: boolean; + noHistoryPush?: boolean; +} + +export const Address = (props: IAddress) => { + const { + address, + isShort, + style, + type = "address", + color = "brand", + displayHash, + } = props; + const history = useHistory(); + const ERC20Map = useERC20Pool(); + const erc721Map = useERC721Pool(); + const erc1155Map = useERC1155Pool(); + const currency = useCurrency(); + + const EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000"; + + if (!address) { + return null; + } + + let parsedName = ""; + + if (ERC20Map[address] && !displayHash) { + parsedName = ERC20Map[address].name; + } + + if (erc721Map[address] && !displayHash) { + parsedName = erc721Map[address].name; + } + + if (erc1155Map[address] && !displayHash) { + parsedName = erc1155Map[address].name; + } + + if (binanceAddressMap[address] && !displayHash) { + parsedName = binanceAddressMap[address]; + } + + parsedName = address === EMPTY_ADDRESS ? "0x0" : parsedName; + + let outPutAddress = address; + try { + outPutAddress = currency === "ONE" ? getAddress(address).bech32 : address; + } catch { + outPutAddress = address; + } + + return ( +
+ + { + e.preventDefault(); + e.stopPropagation(); + toaster.show({ + message: () => ( + + + Copied to clipboard + + ), + }); + }} + /> + history.push(`/${type}/${address}`) + } + > + {parsedName || + (isShort + ? `${outPutAddress.substr(0, 4)}...${outPutAddress.substr(-4)}` + : outPutAddress)} + + +
+ ); +}; diff --git a/src/components/ui/Amount.ts b/src/components/ui/Amount.ts new file mode 100644 index 0000000..9d2c9a4 --- /dev/null +++ b/src/components/ui/Amount.ts @@ -0,0 +1 @@ +export const Amount = () => {} \ No newline at end of file diff --git a/src/components/ui/AnchorLink.tsx b/src/components/ui/AnchorLink.tsx new file mode 100644 index 0000000..02e35eb --- /dev/null +++ b/src/components/ui/AnchorLink.tsx @@ -0,0 +1,14 @@ +import { Anchor, AnchorProps } from 'grommet/components/Anchor' +import React from 'react' +import { Link, LinkProps } from 'react-router-dom' + +export const AnchorLink: React.FC = props => { + return } + {...props} + /> +} + +export type AnchorLinkProps = LinkProps & + AnchorProps & + Omit \ No newline at end of file diff --git a/src/components/ui/BaseContainer.tsx b/src/components/ui/BaseContainer.tsx new file mode 100644 index 0000000..8839e70 --- /dev/null +++ b/src/components/ui/BaseContainer.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { Box } from "grommet"; +import { useMediaQuery } from 'react-responsive'; +import { breakpoints } from "src/Responive/breakpoints"; + +const sizes = { + minWidth: "343px", + maxWidth: "1408px", +}; + +export const BaseContainer = (props: any) => { + const { style } = props; + const isLessTablet = useMediaQuery({ maxDeviceWidth: breakpoints.tablet }); + + return ( + + ); +}; + +export const BasePage = (props: any) => { + const { style } = props; + + return ( + + ); +}; diff --git a/src/components/ui/BlockHash.tsx b/src/components/ui/BlockHash.tsx new file mode 100644 index 0000000..e9815c0 --- /dev/null +++ b/src/components/ui/BlockHash.tsx @@ -0,0 +1,10 @@ +import React from 'react' + +import { Anchor } from 'grommet' +import {AnchorLink} from './AnchorLink' + +// @ts-ignore +export const BlockHash = ({ hash }) => { + const link = `/block/${hash}` + return +} \ No newline at end of file diff --git a/src/components/ui/BlockNumber.tsx b/src/components/ui/BlockNumber.tsx new file mode 100644 index 0000000..19a87c9 --- /dev/null +++ b/src/components/ui/BlockNumber.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { Anchor } from "grommet"; +import { AnchorLink } from "./AnchorLink"; +import { formatNumber } from "."; + +// @ts-ignore + +export const BlockNumber = (options: { number: any; hash?: any }) => { + const { hash, number } = options; + const link = `/block/${hash || number}`; + return ( + + ); +}; diff --git a/src/components/ui/Button/index.tsx b/src/components/ui/Button/index.tsx new file mode 100644 index 0000000..176fff8 --- /dev/null +++ b/src/components/ui/Button/index.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import styled, { css } from "styled-components"; +import { Button as GButton, ButtonExtendedProps } from "grommet"; + +export type TButtonProps = ButtonExtendedProps & { primary?: boolean }; + +export function Button(props: TButtonProps) { + return ; +} + +const StyledButton = styled(GButton)` + border: 1px solid + ${(props) => props.theme.global.colors[props.theme.button.borderColor]}; + padding: 8px 5px; + border-radius: 4px; + font-weight: bold; + text-align: center; + font-size: 12px; + color: ${(props) => props.theme.global.colors.brand}; + transition: 0.3s ease all; + + ${(props) => + props.primary + ? css` + background: ${props.theme.global.colors.backgroundBack}; + ` + : css``}; + + &:hover { + // background-color: ${(props) => props.theme.global.colors.border}; + letter-spacing: 0.3px; + } +`; diff --git a/src/components/ui/CopyBtn.tsx b/src/components/ui/CopyBtn.tsx new file mode 100644 index 0000000..8f14f19 --- /dev/null +++ b/src/components/ui/CopyBtn.tsx @@ -0,0 +1,37 @@ +import { Clone } from "grommet-icons"; +import React from "react"; + +const copyText = (value: string) => { + const copyTextareaInput = document.createElement("textarea"); + copyTextareaInput.value = value; + document.body.appendChild(copyTextareaInput); + + copyTextareaInput.focus(); + copyTextareaInput.select(); + + try { + document.execCommand("copy"); + } catch { + } finally { + document.body.removeChild(copyTextareaInput); + } +}; + +export function CopyBtn(props: { + value: string; + onClick?: (e: React.MouseEvent) => void; +}) { + return ( + { + copyText(props.value); + if (props.onClick) { + props.onClick(e); + } + }} + style={{ cursor: "pointer" }} + /> + ); +} diff --git a/src/components/ui/ERC1155Icon.tsx b/src/components/ui/ERC1155Icon.tsx new file mode 100644 index 0000000..ba2d21f --- /dev/null +++ b/src/components/ui/ERC1155Icon.tsx @@ -0,0 +1,80 @@ +import { Box, Spinner } from "grommet"; +import { Image } from "grommet-icons"; +import React, { useState } from "react"; +import styled from "styled-components"; + +export interface IERC1155IconProps { + imageUrl?: string; +} + +const Loader = styled.div` + position: absolute; + width: 30px; + height: 30px; + background: ${(props) => props.theme.backgroundBack}; +`; + +const InventImg = styled.img` + width: 30px; + height: 30px; + border-radius: 8px; +`; + +const ErrorPreview = styled(Box)` + width: 30px; + height: 30px; + + border-radius: 8px; +`; + +const EmptyImage = styled(Box)` + width: 30px; + height: 30px; + + border-radius: 8px; + background: ${props => props.theme.global.colors.backgroundEmptyIcon}; +`; + +export function ERC1155Icon(props: IERC1155IconProps) { + const [isLoading, setIsLoading] = useState(!!props.imageUrl); + const [isErrorLoading, setIsErrorLoading] = useState(false); + + const url = props.imageUrl + ? `${process.env.REACT_APP_INDEXER_IPFS_GATEWAY}${props.imageUrl}` + : ""; + + return ( + + {isLoading ? ( + + + + + + ) : null} + {isErrorLoading ? ( + + + + ) : url ? ( + setIsLoading(false)} + onError={() => { + setIsLoading(false); + setIsErrorLoading(true); + }} + /> + ) : ( + + )} + + ); +} diff --git a/src/components/ui/ExpandString.tsx b/src/components/ui/ExpandString.tsx new file mode 100644 index 0000000..c5da9ce --- /dev/null +++ b/src/components/ui/ExpandString.tsx @@ -0,0 +1,37 @@ +import React, { useState } from "react"; +import { Box, Text } from "grommet"; + +interface ExpandStringProps { + value: string; + maxLength?: number; +} + +// @ts-ignore +export const ExpandString = (props: ExpandStringProps) => { + const [isFull, setIsFull] = useState(false); + const { value, maxLength = 55 } = props; + + if (value.length < maxLength) { + return {value}; + } + + return ( + + + {isFull ? value : `${value.substr(0, 62)}...`} + + setIsFull(!isFull)} + > + {isFull ? "show less" : "show full"} + + + ); +}; diff --git a/src/components/ui/FiatPrice.tsx b/src/components/ui/FiatPrice.tsx new file mode 100644 index 0000000..4dd8e56 --- /dev/null +++ b/src/components/ui/FiatPrice.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Text } from "grommet"; +import { useONEExchangeRate } from "src/hooks/useONEExchangeRate"; + +export const FiatPrice = () => { + const { lastPrice, priceChangePercent } = useONEExchangeRate(); + + if (!lastPrice) { + return  ; + } + + const price = parseFloat(lastPrice).toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + currency: "USD", + }); + const change = (+priceChangePercent).toFixed(2); + const isPositive = +priceChangePercent >= 0; + + return ( + <> + ONE: ${price}  + + ({isPositive && "+"} + {change}%) + + + ); +}; diff --git a/src/components/ui/HexData.ts b/src/components/ui/HexData.ts new file mode 100644 index 0000000..6f63824 --- /dev/null +++ b/src/components/ui/HexData.ts @@ -0,0 +1 @@ +export const HexData = () => {} \ No newline at end of file diff --git a/src/components/ui/ONEValue.tsx b/src/components/ui/ONEValue.tsx new file mode 100644 index 0000000..df3dd56 --- /dev/null +++ b/src/components/ui/ONEValue.tsx @@ -0,0 +1,81 @@ +import { useONEExchangeRate } from "../../hooks/useONEExchangeRate"; +import { getNearestPriceForTimestamp } from "src/components/ONE_USDT_Rate"; +import { Text, Box, Tip } from "grommet"; +import { TipContent } from "./Tooltip"; +import React from "react"; +import dayjs from "dayjs"; +import { formatNumber } from "./utils"; + +interface ONEValueProps { + value: string | number; + timestamp?: string; + hideTip?: boolean; +} + +// @ts-ignore +export const ONEValue = (props: ONEValueProps) => { + const { value, timestamp = "", hideTip = false } = props; + const { lastPrice } = useONEExchangeRate(); + + if (!value) { + return null; + } + + const isTodayTransaction = + dayjs(timestamp).format("YYYY-MM-DD") === dayjs().format("YYYY-MM-DD"); + const price = + timestamp && !isTodayTransaction + ? getNearestPriceForTimestamp(timestamp) + : lastPrice; + + const bi = BigInt(value) / BigInt(10 ** 14); + const v = parseInt(bi.toString()) / 10000; + let USDValue = ""; + if (price && v > 0) { + USDValue = (v * +price).toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + currency: "USD", + }); + } + + return ( + + 0 ? "bold" : "normal"} + size="small" + margin={{ right: "xxmall" }} + > + {v.toString()} ONE + + {USDValue && +price > 0 && !isTodayTransaction && !hideTip && ( + + {`Displaying value on ${dayjs(timestamp).format( + "YYYY-MM-DD" + )}. Current value`}{" "} + + $ + {formatNumber(v * +lastPrice, { + maximumFractionDigits: 2, + })} + + + } + /> + } + plain + > + (${USDValue}) + + )} + {USDValue && +price > 0 && isTodayTransaction && ( + (${USDValue}) + )} + + ); +}; diff --git a/src/components/ui/OneValueDropdown.tsx b/src/components/ui/OneValueDropdown.tsx new file mode 100644 index 0000000..9482359 --- /dev/null +++ b/src/components/ui/OneValueDropdown.tsx @@ -0,0 +1,107 @@ +import { useONEExchangeRate } from "../../hooks/useONEExchangeRate"; +import { getNearestPriceForTimestamp } from "src/components/ONE_USDT_Rate"; +import { Text, Box, Tip } from "grommet"; +import { TipContent } from "./Tooltip"; +import React from "react"; +import dayjs from "dayjs"; +import { formatNumber } from "./utils"; +import { Dropdown } from "../dropdown/Dropdown"; +import { useThemeMode } from "src/hooks/themeSwitcherHook"; + +interface ONEValueProps { + value: (string | number)[]; + timestamp?: string; + hideTip?: boolean; +} + +// @ts-ignore +export const ONEValueDropdown = (props: ONEValueProps) => { + const { value, timestamp = "", hideTip = false } = props; + const { lastPrice } = useONEExchangeRate(); + const themeMode = useThemeMode(); + + if (!value.length) { + return null; + } + + const isTodayTransaction = + dayjs(timestamp).format("YYYY-MM-DD") === dayjs().format("YYYY-MM-DD"); + const price = + timestamp && !isTodayTransaction + ? getNearestPriceForTimestamp(timestamp) + : lastPrice; + + const normilizedValue: { + value: string | number; + one: number; + usd: string | number; + index: number; + }[] = value.map((hashValue, index) => { + const bi = BigInt(hashValue) / BigInt(10 ** 14); + const v = parseInt(bi.toString()) / 10000; + let USDValue = 0; + if (price && v > 0) { + USDValue = v * +price; + } + + return { value: hashValue, one: v, usd: USDValue || 0, index }; + }); + + return ( + ( + + + + {normilizedValue.reduce((prev, cur) => { + prev += cur.one; + return prev; + }, 0)}{" "} + ONE + + + + ($ + {normilizedValue + .reduce((prev, cur) => { + prev += +cur.usd; + return prev; + }, 0) + .toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + currency: "USD", + })} + ) + + + )} + renderItem={(item) => ( + + + Shard {item.index}:{" "} + + + {item.one} ONE + + {item.usd ? ( + + ($ + {item.usd.toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + currency: "USD", + })} + ) + + ) : null} + + )} + /> + ); +}; diff --git a/src/components/ui/Pagination/index.tsx b/src/components/ui/Pagination/index.tsx new file mode 100644 index 0000000..c7c5c4f --- /dev/null +++ b/src/components/ui/Pagination/index.tsx @@ -0,0 +1,147 @@ +import React, { useEffect } from "react"; +import { Box, Text, Select } from "grommet"; +import { Filter } from "src/types"; +import { FormPrevious, FormNext } from "grommet-icons"; +import { formatNumber } from "src/components/ui/utils"; + +export type TPaginationAction = "nextPage" | "prevPage"; + +interface PaginationNavigator { + filter: Filter; + elements: any[]; + totalElements: number; + onChange: (filter: Filter, action: TPaginationAction) => void; + property?: string; + noScrollTop?: boolean; + showPages?: boolean; + lastElement?: number; + isLoading?: boolean; +} + +export function PaginationNavigator(props: PaginationNavigator) { + const { + elements, + totalElements, + filter, + onChange, + property, + noScrollTop, + showPages, + isLoading, + } = props; + + const { offset = 0, limit = 10 } = filter; + + const onPrevClick = () => { + const newFilter = JSON.parse(JSON.stringify(filter)) as Filter; + newFilter.offset = newFilter.offset - 10; + + if (!isLoading) { + onChange(newFilter, "prevPage"); + } + }; + + const onNextClick = () => { + const newFilter = JSON.parse(JSON.stringify(filter)) as Filter; + newFilter.offset += 10; + if (!isLoading) { + onChange(newFilter, "nextPage"); + } + }; + + return ( + + + + ); +} +interface PaginationProps { + currentPage: number; + totalPages: number; + showPages?: boolean; + onPrevPageClick: () => void; + onNextPageClick: () => void; + disableNextBtn: boolean; + disablePrevBtn: boolean; +} + +function Pagination(props: PaginationProps) { + const { + currentPage, + totalPages, + onPrevPageClick, + onNextPageClick, + showPages, + disableNextBtn, + disablePrevBtn, + } = props; + + return ( + + + {showPages && ( + {formatNumber(+currentPage)} + )} + {showPages && /} + {showPages && ( + {formatNumber(+totalPages)} + )} + + + ); +} + +interface ElementsPerPage { + filter: Filter; + onChange: (filter: Filter) => void; + options?: number[]; +} + +const defaultOptions: string[] = ["10", "25", "50", "100"]; + +export function PaginationRecordsPerPage(props: ElementsPerPage) { + const { filter, options = defaultOptions, onChange } = props; + const { limit = 10 } = filter; + + const onChangeLimit = (props: { option: number }) => { + const newFilter = JSON.parse(JSON.stringify(filter)) as Filter; + newFilter.limit = Number(props.option); + onChange(newFilter); + }; + + return ( + + + +// +// records per page +// +// ); +// } diff --git a/src/components/ui/RelativeTimer.tsx b/src/components/ui/RelativeTimer.tsx new file mode 100644 index 0000000..d0f7226 --- /dev/null +++ b/src/components/ui/RelativeTimer.tsx @@ -0,0 +1,86 @@ +import React, { CSSProperties, useEffect, useState } from "react"; +import { Text } from "grommet"; +import RelativeTime from "dayjs/plugin/relativeTime"; +import UpdateLocale from "dayjs/plugin/updateLocale"; +import dayjs from "dayjs"; + +dayjs.extend(UpdateLocale); +const config = { + thresholds: [ + { l: "s", r: 3, d: "second" }, + { l: "ss", r: 59, d: "second" }, + { l: "m", r: 1 }, + { l: "mm", r: 59, d: "minute" }, + { l: "h", r: 1 }, + { l: "hh", r: 23, d: "hour" }, + { l: "d", r: 1 }, + { l: "dd", r: 29, d: "day" }, + { l: "M", r: 1 }, + { l: "MM", r: 11, d: "month" }, + { l: "y" }, + { l: "yy", d: "year" }, + ], +}; + +dayjs.extend(RelativeTime, config); + +dayjs.updateLocale("en", { + relativeTime: { + future: "in %s", + past: "%s ago", + s: "a few seconds", + ss: "%d seconds", + m: "a minute", + mm: "%d minutes", + h: "an hour", + hh: "%d hours", + d: "a day", + dd: "%d days", + M: "a month", + MM: "%d months", + y: "a year", + yy: "%d years", + }, +}); + +interface IRelativeTimer { + date: number | string | Date; + updateInterval?: number; + style?: CSSProperties; + render?: (value: string) => React.ReactNode; +} + +export function RelativeTimer(props: IRelativeTimer) { + const { date, render, updateInterval = 1000, style } = props; + useEffect(() => { + const getTimeOffset = () => { + setFormattedValue(dayjs().to(dayjs(date))); + }; + getTimeOffset(); + const tId = window.setInterval(getTimeOffset, updateInterval); + + return () => { + clearInterval(tId); + }; + }, [date]); + + const [formattedValue, setFormattedValue] = useState(""); + + if(!date) { + return null; + } + + if (render) { + return
{render(formattedValue)}
; + } + + return ( + + {formattedValue} + + ); +} diff --git a/src/components/ui/Search.tsx b/src/components/ui/Search.tsx new file mode 100644 index 0000000..1357304 --- /dev/null +++ b/src/components/ui/Search.tsx @@ -0,0 +1,301 @@ +import React, { useCallback, useState, useEffect } from "react"; +import { Search } from "grommet-icons"; +import { Box, TextInput, Text } from "grommet"; +import { useHistory } from "react-router-dom"; +import { + getBlockByHash, + getStakingTransactionByField, + getTransactionByField, +} from "src/api/client"; +import { useThemeMode } from "../../hooks/themeSwitcherHook"; +import { getAddress } from "src/utils"; +import { useERC20Pool } from "src/hooks/ERC20_Pool"; +import { useERC721Pool } from "src/hooks/ERC721_Pool"; +import { useERC1155Pool } from "src/hooks/ERC1155_Pool"; + +import { FixedSizeList as List } from "react-window"; +import AutoSizer from "react-virtualized-auto-sizer"; +import { Address } from "./Address"; + +let timeoutID: any | null = null; + +export const SearchInput = () => { + const [value, setValue] = useState(""); + const [readySubmit, setReadySubmit] = useState(false); + const [focus, setFocus] = useState(false); + const [results, setResults] = useState([]); + const themeMode = useThemeMode(); + + const erc20Map = useERC20Pool(); + const erc721Map = useERC721Pool(); + const erc1155Map = useERC1155Pool(); + + const dataTest = [ + ...Object.keys(erc1155Map).map((address) => ({ + symbol: erc1155Map[address].symbol, + name: erc1155Map[address].name, + type: "erc1155", + item: erc1155Map[address], + })), + ...Object.keys(erc20Map).map((address) => ({ + symbol: erc20Map[address].symbol, + name: erc20Map[address].name, + type: "erc20", + item: erc20Map[address], + })), + ...Object.keys(erc721Map).map((address) => ({ + symbol: erc721Map[address].symbol, + name: erc721Map[address].name, + type: "erc721", + item: erc721Map[address], + })), + ]; + + const availableShards = (process.env.REACT_APP_AVAILABLE_SHARDS as string) + .split(",") + .map((t) => +t); + + const history = useHistory(); + const onChange = useCallback((event) => { + const { value: newValue } = event.target; + + setValue(newValue); + + clearTimeout(timeoutID); + timeoutID = setTimeout(() => setReadySubmit(true), 200); + }, []); + + useEffect(() => { + setResults( + dataTest.filter((item) => { + if ( + item.name.toLowerCase().indexOf(value.toLowerCase()) >= 0 || + item.symbol.toLowerCase().indexOf(value.toLowerCase()) >= 0 + ) { + return true; + } + }) + ); + }, [value]); + + useEffect(() => { + const exec = async () => { + // todo separate validation + const v = value.split(" ").join("").toLowerCase(); + + setReadySubmit(false); + if ("" + +v === v && +v > 0) { + // is block number + history.push(`/block/${v}`); + setValue('') + return; + } + + if (v.length !== 66 && v.length !== 42) { + return; + } + if (v.length === 42 && /^0x[a-f0-9]+$/.test(v)) { + // address + history.push(`/address/${v}`); + setValue('') + return; + } + + if (v.length === 42 && v.slice(0, 4) === "one1") { + // address + const ethAddress = getAddress(v).basicHex; + + history.push(`/address/${ethAddress}`); + setValue('') + return; + } + + if (v.length === 66 && v[0] === "0" && v[1] === "x") { + // is block hash or tx hash + try { + try { + await Promise.all([ + getBlockByHash([0, v]) + .then((res) => { + if (!res) { + return; + } + history.push(`/block/${v}`); + setValue(""); + }) + .catch(), + getTransactionByField([0, "hash", v]) + .then((res) => { + if (!res) { + return; + } + history.push(`/tx/${v}`); + setValue('') + }) + .catch(), + getStakingTransactionByField([0, "hash", v]).then((res) => { + if (!res) { + return; + } + + history.push(`/staking-tx/${v}`); + setValue('') + }), + ]); + } catch { + await Promise.all( + availableShards + .filter((t) => t !== 0) + .map((shard) => { + return Promise.all([ + getBlockByHash([shard, v]).then((res) => { + if (!res) { + return; + } + history.push(`/block/${v}`); + setValue('') + }), + getTransactionByField([shard, "hash", v]).then((res) => { + if (!res) { + return; + } + history.push(`/tx/${v}`); + setValue('') + }), + getStakingTransactionByField([shard, "hash", v]).then( + (res) => { + if (!res) { + return; + } + + history.push(`/staking-tx/${v}`); + setValue('') + } + ), + ]); + }) + ); + } + + return; + } catch (e) {} + } + }; + + exec(); + }, [readySubmit]); + + const Row = (options: { index: number; style: any }) => { + const { index, style } = options; + return ( +
+ { + history.push(`/address/${results[index].item.address}`); + setValue(""); + }} + > + + Name {results[index].name} | + + + Symbol {results[index].symbol} | + +
+ +
+ ); + }; + + return ( + + setFocus(true)} + onBlur={() => { + setTimeout(() => { + setFocus(false); + }, 100); + }} + onKeyDown={(e) => { + if (e.keyCode === 13) { + onChange(e); + } + }} + color="red" + icon={} + style={{ + backgroundColor: themeMode === "light" ? "white" : "transparent", + fontWeight: 500, + }} + placeholder="Search by Address / Transaction Hash / Block / Token" + /> + {focus && results.length && value ? ( + + + + {results.length} found + + + + {({ height, width }) => ( + + {Row} + + )} + + + ) : null} + + ); +}; diff --git a/src/components/ui/ShardDropdown.tsx b/src/components/ui/ShardDropdown.tsx new file mode 100644 index 0000000..39e70d7 --- /dev/null +++ b/src/components/ui/ShardDropdown.tsx @@ -0,0 +1,34 @@ +import { Box } from "grommet"; +import React from "react"; +import { useThemeMode } from "src/hooks/themeSwitcherHook"; +import { Dropdown } from "../dropdown/Dropdown"; + +export function ShardDropdown(props: { + selected: string; + onClick: (selected: string) => void; +}) { + const themeMode = useThemeMode(); + + return ( + ({ + value: item, + })) || [] + } + renderValue={(dataItem) => ( + {`Shard ${dataItem.value}`} + )} + renderItem={(dataItem) => <>{`Shard ${dataItem.value}`}} + onClickItem={(item) => props.onClick(item.value)} + value={{ value: props.selected }} + itemStyles={{}} + keyField={"value"} + /> + ); +} diff --git a/src/components/ui/StakingTransactionType.tsx b/src/components/ui/StakingTransactionType.tsx new file mode 100644 index 0000000..579d757 --- /dev/null +++ b/src/components/ui/StakingTransactionType.tsx @@ -0,0 +1,23 @@ +import { Text } from "grommet"; +import { StakingTransactionType } from "src/types"; + +interface IStakingTransactionType { + type: StakingTransactionType; +} + +export const StakingTransactionTypeValue = (props: IStakingTransactionType) => { + const { type } = props; + return ( + + {typeMap[type] || type} + + ); +}; + +const typeMap: Record = { + CreateValidator: "Create Validator", + EditValidator: "Edit Validator", + CollectRewards: "Collect Rewards", + Undelegate: "Undelegate", + Delegate: "Delegate", +}; diff --git a/src/components/ui/Timestamp.tsx b/src/components/ui/Timestamp.tsx new file mode 100644 index 0000000..c67d1bc --- /dev/null +++ b/src/components/ui/Timestamp.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { Clock } from "grommet-icons"; +import dayjs from "dayjs"; +import {RelativeTimer} from "./RelativeTimer"; + +interface TimestampProps { + timestamp: string; + withRelative?: boolean; +} + +// @ts-ignore +export const Timestamp = (props: TimestampProps) => { + const { timestamp, withRelative } = props; + return ( + + +  {dayjs(timestamp).format("YYYY-MM-DD, HH:mm:ss")} + {withRelative && , } + + ); +}; diff --git a/src/components/ui/TokenValue.tsx b/src/components/ui/TokenValue.tsx new file mode 100644 index 0000000..d2c94bd --- /dev/null +++ b/src/components/ui/TokenValue.tsx @@ -0,0 +1,56 @@ +import { Text } from "grommet"; +import React from "react"; +import Big from "big.js"; +import { formatNumber as _formatNumber } from "src/components/ui/utils"; + +import { useERC20Pool } from "src/hooks/ERC20_Pool"; +import { useERC721Pool } from "src/hooks/ERC721_Pool"; + +interface ONEValueProps { + value: string | number; + tokenAddress?: string; + style?: React.CSSProperties; + formatNumber?: boolean; + hideSymbol?: boolean; +} + +Big.DP = 21; +Big.NE = -20; +Big.PE = 15; + +// @ts-ignore +export const TokenValue = (props: ONEValueProps) => { + const { + value, + tokenAddress = "", + style, + formatNumber, + hideSymbol = false, + } = props; + const erc20Map = useERC20Pool(); + const erc721Map = useERC721Pool(); + //TODO remove hardcode + const tokenInfo: any = erc20Map[tokenAddress] || + erc721Map[tokenAddress] || { decimals: 14, symbol: "" }; + + if (!("decimals" in tokenInfo)) { + tokenInfo.decimals = 0; + } + + if (value === "0" || value === 0) { + return ; + } + + if (!value) { + return null; + } + + const bi = Big(value).div(10 ** tokenInfo.decimals); + const v = formatNumber ? _formatNumber(bi.toNumber()) : bi.toString(); + + return ( + + {v} {hideSymbol ? null : tokenInfo.symbol} + + ); +}; diff --git a/src/components/ui/TokenValueBalanced.tsx b/src/components/ui/TokenValueBalanced.tsx new file mode 100644 index 0000000..f1712a1 --- /dev/null +++ b/src/components/ui/TokenValueBalanced.tsx @@ -0,0 +1,102 @@ +import { Box, Text } from "grommet"; +import React, { useEffect, useState } from "react"; +import Big from "big.js"; +import { formatNumber as _formatNumber } from "src/components/ui/utils"; + +import { useERC20Pool } from "src/hooks/ERC20_Pool"; +import { useERC721Pool } from "src/hooks/ERC721_Pool"; +import { BinancePairs } from "src/hooks/BinancePairHistoricalPrice"; +import { getBinancePairPrice } from "src/api/client"; +import { IPairPrice } from "src/api/client.interface"; +import { AnchorLink } from "./AnchorLink"; +import { useERC1155Pool } from "src/hooks/ERC1155_Pool"; + +interface ONEValueProps { + value: string | number; + tokenAddress?: string; + style?: React.CSSProperties; + formatNumber?: boolean; + direction?: "row" | "column"; +} + +Big.DP = 40; +Big.NE = -20; +Big.PE = 20; + +// @ts-ignore +export const TokenValueBalanced = (props: ONEValueProps) => { + const [dollar, setDollar] = useState({} as any); + const { value, tokenAddress = "", style, formatNumber } = props; + const erc20Map = useERC20Pool(); + const erc721Map = useERC721Pool(); + const erc1155Map = useERC1155Pool(); + const { direction = "column" } = props; + + let pairSymbol = BinancePairs.find( + (item) => item.hrc20Address === tokenAddress + ); + + useEffect(() => { + const getContracts = async () => { + try { + let contracts: any = await (pairSymbol?.symbol + ? getBinancePairPrice([`${pairSymbol?.symbol}USDT`]) + : Promise.resolve({})); + + setDollar(contracts); + } catch (err) { + setDollar({} as any); + } + }; + getContracts(); + }, [pairSymbol?.symbol]); + + //TODO remove hardcode + const tokenInfo: any = erc20Map[tokenAddress] || + erc721Map[tokenAddress] || + erc1155Map[tokenAddress] || { decimals: 14, symbol: "" }; + + if (!("decimals" in tokenInfo)) { + tokenInfo.decimals = 0; + } + + if (!value) { + return null; + } + + const dollarPrice = + dollar && dollar.lastPrice + ? Big(value) + .times(+dollar.lastPrice) + .div(10 ** tokenInfo.decimals) + : 0; + + const bi = Big(value).div(10 ** tokenInfo.decimals); + const v = formatNumber ? _formatNumber(bi.toNumber()) : bi.toString(); + + return ( + + + {dollar && dollar.lastPrice ? ( + + + {`${v}`} + + + + {`($${dollarPrice.toFixed(2).toString()})`} + + + ) : ( + + {`${v}`}{" "} + + + )} + + + ); +}; diff --git a/src/components/ui/Tooltip.tsx b/src/components/ui/Tooltip.tsx new file mode 100644 index 0000000..3a7785b --- /dev/null +++ b/src/components/ui/Tooltip.tsx @@ -0,0 +1,13 @@ +import React from 'react' + +import { grommet, Box, Button, Grommet, Heading, Text, Tip } from 'grommet' +import { Trash } from 'grommet-icons' + +// @ts-ignore +export const TipContent = ({ message }) => ( + + +
{message}
+
+
+) \ No newline at end of file diff --git a/src/components/ui/TransactionHash.tsx b/src/components/ui/TransactionHash.tsx new file mode 100644 index 0000000..e17c920 --- /dev/null +++ b/src/components/ui/TransactionHash.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +import { AnchorLink } from "./AnchorLink"; + +// @ts-ignore +export const TransactionHash = ({ hash, link = "tx" }) => { + const url = `/${link}/${hash}`; + return ; +}; diff --git a/src/components/ui/TransactionType.tsx b/src/components/ui/TransactionType.tsx new file mode 100644 index 0000000..d793e1b --- /dev/null +++ b/src/components/ui/TransactionType.tsx @@ -0,0 +1,23 @@ +import { Text } from "grommet"; +import { TraceCallTypes } from "src/types"; + +interface ITransactionType { + type: TraceCallTypes; +} + +export const TransactionType = (props: ITransactionType) => { + const { type } = props; + return ( + + {typeMap[type] || type} + + ); +}; + +const typeMap: Record = { + call: "Call", + staticcall: "Static Call", + create: "Create", + create2: "Create 2", + delegatecall: "Delegate Call", +}; diff --git a/src/components/ui/TxStatusComponent.tsx b/src/components/ui/TxStatusComponent.tsx new file mode 100644 index 0000000..0bde158 --- /dev/null +++ b/src/components/ui/TxStatusComponent.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { Box, Text } from "grommet"; +import { CircleAlert, StatusGood } from "grommet-icons"; + +export function TxStatusComponent(props: { msg?: string }) { + const { msg } = props; + return msg ? ( + + + + + Error + + + + {msg} + + + ) : ( + + + + Success + + + ); +} diff --git a/src/components/ui/icons.tsx b/src/components/ui/icons.tsx new file mode 100644 index 0000000..e08c5e7 --- /dev/null +++ b/src/components/ui/icons.tsx @@ -0,0 +1,106 @@ +import React from "react"; +import { ThemeContext } from "styled-components"; + +interface IIconProps { + size?: string; + color?: string; +} + +export function TelegramIcon(props: IIconProps) { + const theme = React.useContext(ThemeContext); + const { size = "24px", color = theme.global.palette.Grey } = props; + + return ( + + + + ); +} + +export function DiscordIcon(props: IIconProps) { + const theme = React.useContext(ThemeContext); + const { size = "24px", color = theme.global.palette.Grey } = props; + + return ( + + + + + ); +} + +export function LatencyIcon(props: IIconProps) { + const theme = React.useContext(ThemeContext); + const { size = "24px", color = theme.global.palette.Grey } = props; + + return ( + + + + + + + ); +} + +// export function TransactionsIcon(props: IIconProps) { +// const theme = React.useContext(ThemeContext); +// const { size = "24px", color = theme.global.palette.Grey } = props; +// +// return ( +// +// +// +// +// +// +// +// +// ); +// } diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts new file mode 100644 index 0000000..2f5b0da --- /dev/null +++ b/src/components/ui/index.ts @@ -0,0 +1,21 @@ +export * from './Address' +export * from './Timestamp' +export * from './HexData' +export * from './BlockNumber' +export * from './BlockHash' +export * from './TransactionHash' +export * from './AnchorLink' +export * from './Tooltip' +export * from './FiatPrice' +export * from './ONEValue' +export * from './Search' +export * from './BaseContainer' +export * from './Button' +export * from './RelativeTimer' +export * from './Pagination' +export * from './PaginationBlock' +export * from './TransactionType' +export * from './StakingTransactionType'; +export * from './ExpandString'; +export * from './TokenValue'; +export * from './utils' \ No newline at end of file diff --git a/src/components/ui/toaster/Toaster.ts b/src/components/ui/toaster/Toaster.ts new file mode 100644 index 0000000..84a18f3 --- /dev/null +++ b/src/components/ui/toaster/Toaster.ts @@ -0,0 +1,31 @@ +export interface IToasterProps {} +export interface IToasterOption { + message: string | (() => JSX.Element); + /** + * time life in ms. + * if 0 - forever + * @default 3000 + */ + time?: number; +} + +export class Toaster { + public currentSelected: IToasterOption[] = []; + + public updateComponent!: Function; + + show(options: IToasterOption) { + const { time = 3000 } = options; + this.currentSelected.push(options); + this.updateComponent(); + + if (time) { + setTimeout(() => this.hide(options), time); + } + } + + hide(options: IToasterOption) { + this.currentSelected.splice(this.currentSelected.indexOf(options), 1); + this.updateComponent(); + } +} diff --git a/src/components/ui/toaster/ToasterComponent.tsx b/src/components/ui/toaster/ToasterComponent.tsx new file mode 100644 index 0000000..1b65306 --- /dev/null +++ b/src/components/ui/toaster/ToasterComponent.tsx @@ -0,0 +1,77 @@ +import React, { useEffect, useState } from "react"; +import { Box, Text } from "grommet"; +import { Toaster } from "./Toaster"; +import styled, { css, keyframes } from "styled-components"; + +const Wrapper = styled(Box)` + overflow: hidden; +`; + +const animation = keyframes` + from { + transform: translateY(100%); + visibility: visible; + } + + to { + transform: translateY(0); + } +`; +const ToasterItem = styled(Box)<{ index: number }>` + position: absolute; + right: 0px; + bottom: 0px; + height: 60px; + width: 300px; + z-index: 999; + margin: 10px; + + ${(props) => + props.index + ? css` + animation-name: ${animation}; + animation-duration: 0.4s; + animation-fill-mode: both; + ` + : css``}; +`; + +export interface IToasterComponentProps { + toaster: Toaster; +} + +export class ToasterComponent extends React.Component { + constructor(props: IToasterComponentProps) { + super(props); + props.toaster.updateComponent = () => this.forceUpdate(); + } + + render() { + const { currentSelected } = this.props.toaster; + return ( + + {currentSelected.length + ? currentSelected.map((item, index) => { + return ( + + + {typeof item.message === "function" + ? item.message() + : item.message} + + + ); + }) + : null} + + ); + } +} diff --git a/src/components/ui/toaster/index.ts b/src/components/ui/toaster/index.ts new file mode 100644 index 0000000..c5eef99 --- /dev/null +++ b/src/components/ui/toaster/index.ts @@ -0,0 +1,2 @@ +export * from './Toaster' +export * from './ToasterComponent' \ No newline at end of file diff --git a/src/components/ui/utils.tsx b/src/components/ui/utils.tsx new file mode 100644 index 0000000..83eda1f --- /dev/null +++ b/src/components/ui/utils.tsx @@ -0,0 +1,68 @@ +import Big from "big.js"; +import React from "react"; +import { useONEExchangeRate } from "src/hooks/useONEExchangeRate"; + +export function formatNumber(num: number, options?: Intl.NumberFormatOptions): string { + if (num === undefined) return ""; + + return num.toLocaleString("en-US", options); +} + +export function reintervate( + func: () => any, + interval: number +): number | Promise { + const res = func(); + if (res instanceof Promise) { + return res.then(() => + window.setTimeout(() => reintervate(func, interval), interval) + ); + } else { + return window.setTimeout(() => reintervate(func, interval), interval); + } +} + +Big.DP = 21; +Big.NE = -20; +Big.PE = 15; + +export function CalculateFee(transaction: any) { + const { lastPrice } = useONEExchangeRate(); + + const fee = + isNaN(transaction.gas) || isNaN(transaction.gasPrice) + ? 0 + : (Number(transaction.gas) * Number(transaction.gasPrice)) / + 10 ** 14 / + 10000; + + const normolizedFee = Intl.NumberFormat("en-US", { + maximumFractionDigits: 18, + }).format(fee); + + const price = lastPrice; + + const bi = + ((Big(normolizedFee) as unknown) as number) / + ((Big(10 ** 14) as unknown) as any); + const v = parseInt(bi.toString()) / 10000; + let USDValue = ""; + + if (price && v > 0) { + USDValue = (v * +price).toLocaleString("en-US", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + currency: "USD", + }); + } + + return ( + <> + {Intl.NumberFormat("en-US", { maximumFractionDigits: 18 }).format(fee)} + {!USDValue || USDValue === "0.00" || USDValue == "0" ? null : ( + <>($ {USDValue}) + )} + + ); + //return Math.round(fee * 10 ** 9) / 10 ** 9; +} diff --git a/src/config/BinanceAddressMap.ts b/src/config/BinanceAddressMap.ts new file mode 100644 index 0000000..acb1bcb --- /dev/null +++ b/src/config/BinanceAddressMap.ts @@ -0,0 +1,7 @@ +export const binanceAddressMap: any = { + "0x71b413da5cc729ff805e2dca1dcde04d29ef2b6a": "Binance Gateway", + "0x1548c6227cbd78e51eb0a679c1f329b9a5a99beb": "DaVinci Images", + "0xbde650853b535d738ce67f1bdeb335e38834a9e9": "DaVinci Music", + "0x474d8fd12780fbe2b7b7bd74eb326bb75ded91d8": "DaVinci Videos", + "0x51f6290510be3c802471e27f0843a3a54a8226df": "DaVinci Books", +}; diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..2549cd9 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1 @@ +export const config = {} \ No newline at end of file diff --git a/src/hooks/BinancePairHistoricalPrice.ts b/src/hooks/BinancePairHistoricalPrice.ts new file mode 100644 index 0000000..06e02c0 --- /dev/null +++ b/src/hooks/BinancePairHistoricalPrice.ts @@ -0,0 +1,32 @@ +import { useState } from "react"; +import { singletonHook } from "react-singleton-hook"; + +export const BinancePairs: Erc20Price[] = JSON.parse( + '[\n {\n "symbol": "BUSD",\n "hrc20Address": "0xe176ebe47d621b984a73036b9da5d834411ef734"\n },\n {\n "symbol": "ETH",\n "hrc20Address": "0x6983D1E6DEf3690C4d616b13597A09e6193EA013"\n },\n {\n "symbol": "LINK",\n "hrc20Address": "0x218532a12a389a4a92fc0c5fb22901d1c19198aa"\n },\n {\n "symbol": "MAGGOT",\n "hrc20Address": "0xBfD4F1699b83eDBa1106B6E224b7aC599A40be1F"\n },\n {\n "symbol": "USDT",\n "hrc20Address": "0x3C2B8Be99c50593081EAA2A724F0B8285F5aba8f"\n },\n {\n "symbol": "UNI",\n "hrc20Address": "0x90D81749da8867962c760414C1C25ec926E889b6"\n },\n {\n "symbol": "YFI",\n "hrc20Address": "0xa0dc05F84A27FcCBD341305839019aB86576bc07"\n },\n {\n "symbol": "MEME",\n "hrc20Address": "0x43ba3c1eb73FfECFbF22c44C21b26CdE1012353a"\n },\n {\n "symbol": "DAI",\n "hrc20Address": "0xEf977d2f931C1978Db5F6747666fa1eACB0d0339"\n },\n {\n "symbol": "USDC",\n "hrc20Address": "0x985458E523dB3d53125813eD68c274899e9DfAb4"\n },\n {\n "symbol": "KEEP",\n "hrc20Address": "0x43bF77db5e784b263a459141BdCDf5Cf6987936d"\n },\n {\n "symbol": "SUSHI",\n "hrc20Address": "0xBEC775Cb42AbFa4288dE81F387a9b1A3c4Bc552A"\n },\n {\n "symbol": "ROT",\n "hrc20Address": "0xFd2a8F8cF7CFFeA4a613F1DFf39b22881D4a1f92"\n },\n {\n "symbol": "EMN",\n "hrc20Address": "0xaB318f90bDBC755bCbBC3db66428f457a5f9bC59"\n },\n {\n "symbol": "YAM",\n "hrc20Address": "0x7202ADF025CbD1cC9411fd56E3Cc8EF2E9dFFA9D"\n },\n {\n "symbol": "BAT",\n "hrc20Address": "0x2875B4CfAb0A4cc4bdc7fBDf94b6E376826A4332"\n },\n {\n "symbol": "COMP",\n "hrc20Address": "0x32137b9275EA35162812883582623cd6f6950958"\n },\n {\n "symbol": "WBTC",\n "hrc20Address": "0x3095c7557bCb296ccc6e363DE01b760bA031F2d9"\n },\n {\n "symbol": "KNC",\n "hrc20Address": "0x0a47D2dC4B7Ee3D4D7FD471d993b0821621e1769"\n },\n {\n "symbol": "ZRX",\n "hrc20Address": "0x8143E2A1085939cAA9cEf6665c2Ff32f7bc08435"\n },\n {\n "symbol": "CRO",\n "hrc20Address": "0x2672B791D23879995AaBdf51Bc7d3DF54BB4e266"\n },\n {\n "symbol": "AAVE",\n "hrc20Address": "0xcF323Aad9E522B93F11c352CaA519Ad0E14eB40F"\n },\n {\n "symbol": "REN",\n "hrc20Address": "0x451E129b6045b6E4F48E7247388f21163f7743B7"\n },\n {\n "symbol": "GST2",\n "hrc20Address": "0x8e34D1351ef8DB0AcAC008761075F33D072Cd753"\n },\n {\n "symbol": "HGT",\n "hrc20Address": "0x006f5E4c3d97d6d2652CF10BC08Fd779d680B00B"\n },\n {\n "symbol": "MATIC",\n "hrc20Address": "0x301259f392B551CA8c592C9f676FCD2f9A0A84C5"\n },\n {\n "symbol": "SWAG",\n "hrc20Address": "0xC63F45B5F6D63D8c424E408ACFB4B5823955F12A"\n },\n {\n "symbol": "BAL",\n "hrc20Address": "0xDc5f76104D0B8D2bF2c2Bbe06CDFE17004E9010f"\n },\n {\n "symbol": "LAYER",\n "hrc20Address": "0xE88699AD32d5A610987a3BA8519C06289549cCa7"\n },\n {\n "symbol": "ABYSS",\n "hrc20Address": "0xA52D0c7943cc1020A926B23dd1c64Fc60b4fadDe"\n },\n {\n "symbol": "AMPL",\n "hrc20Address": "0xF2f5BF00cd952f3f980a02f5dce278CbFf4daE05"\n },\n {\n "symbol": "WETH",\n "hrc20Address": "0xF720b7910C6b2FF5bd167171aDa211E226740bfe"\n },\n {\n "symbol": "KICK",\n "hrc20Address": "0xC63Fd3e9C9527cCdF1d331bbADFE21E77E357B5E"\n },\n {\n "symbol": "FRONT",\n "hrc20Address": "0x1eE5839950Fd7a227f91CF679b1931dd6F5798B3"\n },\n {\n "symbol": "SAND",\n "hrc20Address": "0x35de8649e1e4Fd1A7Bd3B14F7e24e5e7887174Ed"\n },\n {\n "symbol": "IDRT",\n "hrc20Address": "0xCEFbeA899CfcCdc653b171d063481b622086Be3F"\n },\n {\n "symbol": "CEL",\n "hrc20Address": "0xd562c88E0f8E7dAe43076018Bb1ea3115617984D"\n },\n {\n "symbol": "PAXG",\n "hrc20Address": "0x7aFB0E2ebA6Dc938945FE0f42484d3b8F442D0AC"\n },\n {\n "symbol": "UNFI",\n "hrc20Address": "0xa1c05343Ecc902568248DdF837B7735a800aDA23"\n },\n {\n "symbol": "UP",\n "hrc20Address": "0xBF33E24DceC9C884e286e3ad1D5C7C7f1f4D7afD"\n },\n {\n "symbol": "HEX",\n "hrc20Address": "0xf26D8c787e66254eE8B7A500073da8fb1ab1992D"\n },\n {\n "symbol": "STORJ",\n "hrc20Address": "0x266F341E33aa61c30c6a9af89314811A5b097cb4"\n },\n {\n "symbol": "SMC",\n "hrc20Address": "0x3A3ebAA3EdCd7e6f7C8139A60D4009aB7DcDeDFe"\n },\n {\n "symbol": "VI",\n "hrc20Address": "0xF3f8089362dD4C0FD56f12B7D34eDBddc7a12D6F"\n },\n {\n "symbol": "YFL",\n "hrc20Address": "0x421b372389f89E2Abbf2C91f006Fc04a1f424B2D"\n },\n {\n "symbol": "1ONE",\n "hrc20Address": "0x674B0e93D596F4025848D3fa907c839e42850dc7"\n },\n {\n "symbol": "ARCD",\n "hrc20Address": "0x90edb740e0F02C532Fa90B7ED69Cbd4601F54A5c"\n },\n {\n "symbol": "UNISTAKE",\n "hrc20Address": "0x1Ec56F91d61F2c2CB1af0a2B9BB8d7984F0A6626"\n },\n {\n "symbol": "COMBO",\n "hrc20Address": "0x5693FE17Ad04F0D8F768cEeB863e62B522901440"\n },\n {\n "symbol": "HOT",\n "hrc20Address": "0x5dfEaDCDD2d4eB29aC5Ae876dAA07FfD07Bf6483"\n },\n {\n "symbol": "HT",\n "hrc20Address": "0xBAA0974354680B0e8146d64bB27Fb92C03C4A2f2"\n },\n {\n "symbol": "TAO$",\n "hrc20Address": "0x0A5bd477184989ecCFb345a1d3ce3CC6a80bF29b"\n },\n {\n "symbol": "DSLA",\n "hrc20Address": "0x34704c70e9eC9fB9A921da6DAAD7D3e19f43c734"\n },\n {\n "symbol": "LOTTO",\n "hrc20Address": "0x1f7178D868ED7da31f8E8a15d9f499bCBdaEFFf2"\n },\n {\n "symbol": "MARK",\n "hrc20Address": "0xE52c7144fEd0aeCa882dD93Bc76c9135f5598DDD"\n },\n {\n "symbol": "SRK",\n "hrc20Address": "0x9500A1FbEF7014dbD384633fD20bb1e6916d6Fca"\n },\n {\n "symbol": "LINA",\n "hrc20Address": "0x946c8286bd9B52B81F681903210E1a57872fdD85"\n },\n {\n "symbol": "DEXE",\n "hrc20Address": "0xEaB6222a2823339e6952562eB75e56c6c8eeA36B"\n },\n {\n "symbol": "ONE",\n "hrc20Address": "0x3AC01098415C0CCf479729022D07d5aC3B048b73"\n },\n {\n "symbol": "BADGER",\n "hrc20Address": "0x06b19a0ce12dC71f1C7a6DD39E8983E089C40E0d"\n },\n {\n "symbol": "SXP",\n "hrc20Address": "0x77d046614710fdDf5CA3E3cE85F4f09f7ABC283c"\n },\n {\n "symbol": "SNX",\n "hrc20Address": "0x7b9c523d59AeFd362247Bd5601A89722e3774dD2"\n },\n {\n "symbol": "EBOX",\n "hrc20Address": "0x4328588AaE1108FBD36e5cdB57C8128dcF7a6D9A"\n },\n {\n "symbol": "FTM",\n "hrc20Address": "0x39aB439897380eD10558666C4377fACB0322Ad48"\n },\n {\n "symbol": "WISE",\n "hrc20Address": "0xE7e3C4D1cFc722b45A428736845B6AfF862842a1"\n },\n {\n "symbol": "AiDAO",\n "hrc20Address": "0x1b242b9f11764fCc99aad68721c439c7E3cb59aa"\n },\n {\n "symbol": "REEF",\n "hrc20Address": "0x9aB0DB833557d95AFf98C09B560145Ad34E681b8"\n },\n {\n "symbol": "dART",\n "hrc20Address": "0x89102770BB34393B30bEC0F82a6cD2587131552F"\n },\n {\n "symbol": "1INCH",\n "hrc20Address": "0x58f1b044d8308812881a1433d9Bbeff99975e70C"\n },\n {\n "symbol": "TUSD",\n "hrc20Address": "0x553A1151F3Df3620fC2B5A75A6edDa629e3dA350"\n },\n {\n "symbol": "ROOK",\n "hrc20Address": "0x08310baF9DeB5F13B885eDf5eeA6fD19DC21AF3A"\n },\n {\n "symbol": "BUNNY",\n "hrc20Address": "0x758765664769D82380c95CC3489d533FA21974fa"\n },\n {\n "symbol": "TVK",\n "hrc20Address": "0xced019c13AeC9dC2C641FC3Bb0B24C7231f0fFF1"\n },\n {\n "symbol": "GRT",\n "hrc20Address": "0x002FA662F2E09de7C306d2BaB0085EE9509488Ff"\n },\n {\n "symbol": "DOGEN",\n "hrc20Address": "0x93C10C4A4436c8F6e5A9d1325B570e84df7fcEDb"\n },\n {\n "symbol": "EASY",\n "hrc20Address": "0xB10E699aE2D80607147BCc617b6A3B64D3a434Bd"\n },\n {\n "symbol": "LYXe",\n "hrc20Address": "0x8A8ca151562a68ED3732Fd963Ec4e0E713B39BB3"\n },\n {\n "symbol": "LPT",\n "hrc20Address": "0xBD3E698b51D340Cc53B0CC549b598c13e0172B7c"\n },\n {\n "symbol": "ENJ",\n "hrc20Address": "0xadbd41bFb4389dE499535C14A8a3A12Fead8F66A"\n },\n {\n "symbol": "CHAIN",\n "hrc20Address": "0x84Ec08c887DD8c14D389abe56e609379B7C5262E"\n },\n {\n "symbol": "PETRON",\n "hrc20Address": "0xD8ecd9D2ef5dA0327c2D834B8D118febCce03F4c"\n },\n {\n "symbol": "ARC",\n "hrc20Address": "0x3DcA48E4707cb2f08EfbE836062AAa45cbB851cB"\n },\n {\n "symbol": "MCO",\n "hrc20Address": "0xbce4F76227D9b97bAabA1e1a4021d20979ED03ec"\n },\n {\n "symbol": "IMX",\n "hrc20Address": "0xBD8064CdB96C00A73540922504F989c64B7b8B96"\n },\n {\n "symbol": "DRC",\n "hrc20Address": "0xe74A0BA232a62Ddb80e53EA7ed9B799445F52876"\n },\n {\n "symbol": "FEG",\n "hrc20Address": "0x3042BB02E308431e1D8cD5785A60BbD7ED2E3f54"\n },\n {\n "symbol": "KISHU",\n "hrc20Address": "0x147E4Bc895Dc5995Be2C8523A3ED6FF708beAeC3"\n },\n {\n "symbol": "renBTC",\n "hrc20Address": "0x41CA97b94D5deE79195856034D196dDfa0D43EDD"\n },\n {\n "symbol": "JPYC",\n "hrc20Address": "0x951C343A392633c8CEfB0B7F855EAD9a8f8c72A1"\n },\n {\n "symbol": "BNB",\n "hrc20Address": "0xb1f6E61E1e113625593a22fa6aa94F8052bc39E0"\n },\n {\n "symbol": "BUSD",\n "hrc20Address": "0x0aB43550A6915F9f67d0c454C2E90385E6497EaA"\n },\n {\n "symbol": "ETH",\n "hrc20Address": "0x783EE3E955832a3D52CA4050c4C251731c156020"\n },\n {\n "symbol": "USDT",\n "hrc20Address": "0x9A89d0e1b051640C6704Dde4dF881f73ADFEf39a"\n },\n {\n "symbol": "MOCHI",\n "hrc20Address": "0xda73f5C25C0D644Afd20dA5535558956B192b262"\n },\n {\n "symbol": "ADA",\n "hrc20Address": "0x582617bD8Ca80d22D4432E63Fda52D74dcDCEe4c"\n },\n {\n "symbol": "SAFEMOON",\n "hrc20Address": "0x58c5E26fcc4d1d442396D33b58af31549C64d22F"\n },\n {\n "symbol": "JulD",\n "hrc20Address": "0x504D7d5bd2075FA782Fbd0bE9bEA4CDC7e25f5a1"\n },\n {\n "symbol": "REEF",\n "hrc20Address": "0xA2d18722e96B3622d28f4d1bd9631D4B75f4186C"\n },\n {\n "symbol": "BAKE",\n "hrc20Address": "0x4da9464DaF2b878e32E29115E2cFD786fE84692a"\n },\n {\n "symbol": "Cake",\n "hrc20Address": "0x3e9D32580B0BF3aE72AFCBEbC68710d2Fd9a18F0"\n },\n {\n "symbol": "APESOX",\n "hrc20Address": "0x53051d5545745F600232a885a65479cA832198fb"\n },\n {\n "symbol": "LINK",\n "hrc20Address": "0x88B0811DdeC7c94Cc48dE601BdAbd1AC37d6940B"\n },\n {\n "symbol": "FEG",\n "hrc20Address": "0xC7eaAa7FC41Fa4D814192979f267D80CB48fB760"\n },\n {\n "symbol": "APEDOGE",\n "hrc20Address": "0x6F6Fb59148a982F215c48852f7772a5664C695AD"\n },\n {\n "symbol": "APESAFE",\n "hrc20Address": "0x52Ac8c6e34CAE594af1F57b103Bb06328a23033C"\n },\n {\n "symbol": "USDC",\n "hrc20Address": "0x44cED87b9F1492Bf2DCf5c16004832569f7f6cBa"\n },\n {\n "symbol": "TWOK",\n "hrc20Address": "0xAe1b8C2A966666BFe8bB1C78860bA27f92d5B8eA"\n },\n {\n "symbol": "Mochi-LP",\n "hrc20Address": "0xe2707c50A928f5f2c9B09d6e9387c578dE9206B4"\n },\n {\n "symbol": "DAI",\n "hrc20Address": "0x1d374ED0700a0aD3cd4945D66a5B1e08e5db20A8"\n },\n {\n "symbol": "BELUGA",\n "hrc20Address": "0x2F20a4022Bf71daf47eC43972B6ecF56e0DB0609"\n },\n {\n "symbol": "PIG",\n "hrc20Address": "0x65B519d5fB8DaDaB9d9994D3A8aebfdf43910Aea"\n },\n {\n "symbol": "CHI",\n "hrc20Address": "0x2662eB1b5A4Cf2AbFB5fa2AFE75dA715e82b483f"\n },\n {\n "symbol": "SUSHI",\n "hrc20Address": "0x547B2D8b4165d2c5Ab75cdb6B6C2B313D9724a11"\n },\n {\n "symbol": "VIPER",\n "hrc20Address": "0x84e13630160868A3bAe7b7DE33eB730BFf311c64"\n },\n {\n "symbol": "ONX",\n "hrc20Address": "0x7Ca40b38EC544d75de186e826c1e2633c9988941"\n },\n {\n "symbol": "NVG",\n "hrc20Address": "0xF23b931BE00574129E4C3e055f9E5483839dc54a"\n },\n {\n "symbol": "WBNB",\n "hrc20Address": "0x673D2EC54E0a6580fc7E098295B70e3cE0350D03"\n },\n {\n "symbol": "ElonGate",\n "hrc20Address": "0x11FC804f435c601335b55189fE519fdb88f23A29"\n },\n {\n "symbol": "ONE",\n "hrc20Address": "0xAD785c955625E4e82cdAE2F997cd6038A8ecd90E"\n },\n {\n "symbol": "AAVE",\n "hrc20Address": "0x6Fc97bA83085FCb9a60bac8151418d51b470647d"\n },\n {\n "symbol": "BUSD",\n "hrc20Address": "0x791fA343792F377369a0189Fc69aFa14ad12d3AC"\n },\n {\n "symbol": "YFI",\n "hrc20Address": "0xB74F84131d2A980375629e0c8589034F5330667D"\n },\n {\n "symbol": "1INCH",\n "hrc20Address": "0x8DdFF1C168fd099504B0d817837B10F58BE67d26"\n },\n {\n "symbol": "UNI",\n "hrc20Address": "0x775020Dd09A94414a8c1A89c5a3d3c9d1B22529d"\n },\n {\n "symbol": "SNX",\n "hrc20Address": "0x1D1bAef88c0Dcdf4d99f3df9dBB7DB90f30C2cFe"\n },\n {\n "symbol": "MATIC",\n "hrc20Address": "0x6E7bE5B9B4C9953434CD83950D61408f1cCc3bee"\n },\n {\n "symbol": "1bscBUSD",\n "hrc20Address": "0x332B4558A675E344b3CC5BE5D72d5Fa71Ab8dE32"\n },\n {\n "symbol": "1bscMATIC",\n "hrc20Address": "0x0000000000000000000000000000000000000000"\n },\n {\n "symbol": "bBEAR",\n "hrc20Address": "0x133F72321C1A8cd8300c6e67eA70074d8bfEbf82"\n },\n {\n "symbol": "bDOGEN",\n "hrc20Address": "0xf2D1Db1CD4E15C6448dEE43aB9Af9eE72a237Aea"\n },\n {\n "symbol": "1bscMOCHI",\n "hrc20Address": "0x3C2294d9e384E8d6584353348140253613b37920"\n },\n {\n "symbol": "TAD",\n "hrc20Address": "0x6a154903B23D6043D168D3d5374618c0187cC9ec"\n },\n {\n "symbol": "LAIKA",\n "hrc20Address": "0x6AE269cFD931180a00E4A1ec6461Ff587ad57647"\n },\n {\n "symbol": "DOGE",\n "hrc20Address": "0xF155E1a57DB0Ca820aE37Ab4050e0e4C7cFcEcd0"\n },\n {\n "symbol": "BTCB",\n "hrc20Address": "0x34224dCF981dA7488FdD01c7fdd64E74Cd55DcF7"\n },\n {\n "symbol": "WISB",\n "hrc20Address": "0x8F0D210bf0b8A3A3E4B1012B21e6974818a35B94"\n },\n {\n "symbol": "1bscUSDT",\n "hrc20Address": "0x6515d0427e5583CA085dF3879A18623A234d9790"\n },\n {\n "symbol": "BUNGA",\n "hrc20Address": "0x9406982C1c5d1007feb194a8a791c7F453160BAC"\n },\n {\n "symbol": "JPYC",\n "hrc20Address": "0xe112b5f917e882033084f64615fb9a3B37e617Bb"\n },\n {\n "symbol": "ATOM",\n "hrc20Address": "0xd6bAd903e550822d51073AFb79581BF5aAE9243F"\n },\n {\n "symbol": "UST",\n "hrc20Address": "0xb82307fF75F0CD2cFc253BA2621851fd9123a818"\n },\n {\n "symbol": "DogeBSC",\n "hrc20Address": "0xC9E149cF89A1700cd87a3Af2D6B00ec503A668c4"\n },\n {\n "symbol": "11YFI",\n "hrc20Address": "0xEe1158C381dCC13A0808555a93067e5E0a680838"\n },\n {\n "symbol": "1bsc11YFI",\n "hrc20Address": "0x1E973a70CDffb61b8d97ce084664A6aa2b6bC29d"\n }\n]' +); + +let globalSetMode = () => { + return {}; +}; + +export const useBinancePairHistoricalPrice = singletonHook(BinancePairs, () => { + const pool = + (JSON.parse( + window.localStorage.getItem("BinancePairHistoricalPrice") || "[]" + ) as Erc20Price[]) || BinancePairs; + + const [mode, setMode] = useState(pool); + //@ts-ignore + globalSetMode = setMode; + return mode; +}); + +export const setBinancePairHistoricalPrice = (pool: Erc20Price[]) => { + //@ts-ignore + globalSetMode(pool); +}; + +export interface Erc20Price { + symbol: string; + hrc20Address: string; +} diff --git a/src/hooks/ERC1155_Pool.ts b/src/hooks/ERC1155_Pool.ts new file mode 100644 index 0000000..dd4b7c4 --- /dev/null +++ b/src/hooks/ERC1155_Pool.ts @@ -0,0 +1,37 @@ +import { useState } from "react"; +import { singletonHook } from "react-singleton-hook"; + +const initValue: ERC1155_Pool = {}; + +let globalSetMode = () => { + return {}; +}; + +export const useERC1155Pool = singletonHook(initValue, () => { + const pool = + (JSON.parse( + window.localStorage.getItem("ERC1155_Pool") || "{}" + ) as ERC1155_Pool) || initValue; + + const [mode, setMode] = useState(pool); + //@ts-ignore + globalSetMode = setMode; + return mode; +}); + +export const setERC1155Pool = (pool: ERC1155_Pool) => { + //@ts-ignore + globalSetMode(pool); +}; + +export interface ERC1155 { + name: string; + address: string; + totalSupply: string; + holders: string; + decimals: number; + symbol: string; + meta?: any +} + +export type ERC1155_Pool = Record; diff --git a/src/hooks/ERC20_Pool.ts b/src/hooks/ERC20_Pool.ts new file mode 100644 index 0000000..d475a6b --- /dev/null +++ b/src/hooks/ERC20_Pool.ts @@ -0,0 +1,42 @@ +import { useState } from "react"; +import { singletonHook } from "react-singleton-hook"; + +const initValue: ERC20_Pool = {}; + +let globalSetMode = () => { + return {}; +}; + +export const useERC20Pool = singletonHook(initValue, () => { + const pool = + (JSON.parse( + window.localStorage.getItem("ERC20_Pool") || "{}" + ) as ERC20_Pool) || initValue; + + const [mode, setMode] = useState(pool); + //@ts-ignore + globalSetMode = setMode; + return mode; +}); + +export const setERC20Pool = (pool: ERC20_Pool) => { + //@ts-ignore + globalSetMode(pool); +}; + +export interface Erc20 { + name: string; + address: string; + totalSupply: string; + circulating_supply: string + holders: string; + decimals: number; + symbol: string; + lastUpdateBlockNumber: string + meta?: { + name?: string + image?: string; + }; +} + +export type ERC20_Pool = Record; diff --git a/src/hooks/ERC721_Pool.ts b/src/hooks/ERC721_Pool.ts new file mode 100644 index 0000000..4097116 --- /dev/null +++ b/src/hooks/ERC721_Pool.ts @@ -0,0 +1,36 @@ +import { useState } from "react"; +import { singletonHook } from "react-singleton-hook"; + +const initValue: ERC721_Pool = {}; + +let globalSetMode = () => { + return {}; +}; + +export const useERC721Pool = singletonHook(initValue, () => { + const pool = + (JSON.parse( + window.localStorage.getItem("ERC721_Pool") || "{}" + ) as ERC721_Pool) || initValue; + + const [mode, setMode] = useState(pool); + //@ts-ignore + globalSetMode = setMode; + return mode; +}); + +export const setERC721Pool = (pool: ERC721_Pool) => { + //@ts-ignore + globalSetMode(pool); +}; + +export interface ERC721 { + name: string; + address: string; + totalSupply: string; + holders: string; + decimals: number; + symbol: string; +} + +export type ERC721_Pool = Record; diff --git a/src/hooks/ONE-ETH-SwitcherHook.ts b/src/hooks/ONE-ETH-SwitcherHook.ts new file mode 100644 index 0000000..c949831 --- /dev/null +++ b/src/hooks/ONE-ETH-SwitcherHook.ts @@ -0,0 +1,26 @@ +import { useState } from "react"; +import { singletonHook } from "react-singleton-hook"; + +const initCurrency: currencyType = "ONE"; + +let globalSetMode = () => { + throw new Error("you must useDarkMode before setting its state"); +}; + +export const useCurrency = singletonHook(initCurrency, () => { + const currentTheme = window.localStorage.getItem('currency') as currencyType || initCurrency; + + const [mode, setMode] = useState(currentTheme); + //@ts-ignore + globalSetMode = setMode; + return mode; +}); + + +export const setCurrency = (mode: currencyType) => { + //@ts-ignore + globalSetMode(mode); + window.localStorage.setItem('currency', mode); +}; + +export type currencyType = "ONE" | "ETH"; diff --git a/src/hooks/polling.tsx b/src/hooks/polling.tsx new file mode 100644 index 0000000..79b0ae1 --- /dev/null +++ b/src/hooks/polling.tsx @@ -0,0 +1,75 @@ +import { useState, useEffect, useRef, Dispatch } from 'react' + +export type APIPollingOptions = { + fetchFunc: () => Promise + initialState: DataType + delay: number + onError?: (e: Error, setData?: Dispatch) => void + updateTrigger?: any +} + +function useAPIPolling(opts: APIPollingOptions): DataType { + const { initialState, fetchFunc, delay, onError, updateTrigger } = opts + + const timerId = useRef() + const fetchCallId = useRef(0) + const [data, setData] = useState(initialState) + + const fetchData = (id: Number) => { + return new Promise(resolve => { + fetchFunc() + .then(newData => { + if (id === fetchCallId.current) { + setData(newData) + } + resolve(null) + }) + .catch(e => { + if (!onError) { + setData(initialState) + resolve(null) + } else { + onError(e, setData) + resolve(null) + } + }) + }) + } + + const pollingRoutine = () => { + fetchCallId.current += 1 + /* tslint:disable no-floating-promises */ + fetchData(fetchCallId.current).then(() => { + doPolling() + }) + /* tslint:enable no-floating-promises */ + } + + const doPolling = () => { + timerId.current = setTimeout(() => { + pollingRoutine() + }, delay) + } + + const stopPolling = () => { + if (timerId.current) { + clearTimeout(timerId.current) + timerId.current = null + } + } + + useEffect( + () => { + /* tslint:disable no-floating-promises */ + pollingRoutine() + /* tslint:enable */ + + return stopPolling + }, + updateTrigger ? [updateTrigger] : [] + ) + + return data +} + +export default useAPIPolling \ No newline at end of file diff --git a/src/hooks/themeSwitcherHook.ts b/src/hooks/themeSwitcherHook.ts new file mode 100644 index 0000000..4901328 --- /dev/null +++ b/src/hooks/themeSwitcherHook.ts @@ -0,0 +1,26 @@ +import { useState } from "react"; +import { singletonHook } from "react-singleton-hook"; + +const initTheme: themeType = "light"; + +let globalSetMode = () => { + throw new Error("you must useDarkMode before setting its state"); +}; + +export const useThemeMode = singletonHook(initTheme, () => { + const currentTheme = window.localStorage.getItem('themeMode') as themeType || initTheme; + + const [mode, setMode] = useState(currentTheme); + //@ts-ignore + globalSetMode = setMode; + return mode; +}); + + +export const setThemeMode = (mode: themeType) => { + //@ts-ignore + globalSetMode(mode); + window.localStorage.setItem('themeMode', mode); +}; + +export type themeType = "light" | "dark"; diff --git a/src/hooks/useONEExchangeRate.tsx b/src/hooks/useONEExchangeRate.tsx new file mode 100644 index 0000000..0d3db9b --- /dev/null +++ b/src/hooks/useONEExchangeRate.tsx @@ -0,0 +1,23 @@ +import React, {useEffect, useState} from 'react' +import useAPIPolling, { APIPollingOptions } from './polling' +import { singletonHook } from 'react-singleton-hook'; + +const url = 'https://api.binance.com/api/v1/ticker/24hr?symbol=ONEUSDT' +const fetchFunc = () => fetch(url).then(r => r.json()) + +export const useONEExchangeRate = singletonHook({}, () => { + const [data, setData] = useState({}) + + const options: APIPollingOptions = { + fetchFunc, + initialState: {}, + delay: 30000 + } + const res = useAPIPolling(options) + + useEffect(() => { + setData(res) + }, [res]) + + return data +}) \ No newline at end of file diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..56bcf06 --- /dev/null +++ b/src/index.css @@ -0,0 +1,40 @@ +@import-normalize; + +a { + text-decoration: none; + outline: 0; +} + +.g-table-header thead tr th { + padding-top: 0; + padding-bottom: 0; +} + +.g-table-header thead tr th:last-child { + text-align: right; +} + +.g-table-body-last-col-right tbody td:last-child { + text-align: left !important; +} + +.g-table-no-header thead { + display: none; +} +.g-table-body-last-col-right tbody tr td , .g-table-body-last-col-right tbody tr th { + background: transparent !important; +} + +.g-table-header tbody tr td, .g-table-header tbody tr th { + background: transparent !important; +} + +body.light > div > div[aria-hidden=false] > div > div { + background: #fff !important; + border: 1px solid #f3f3f3; +} + +body.dark > div > div[aria-hidden=false] > div > div { + background: #1b2a5e !important; + border: 1px solid #3a54b3; +} \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..b67d3fb --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import App from './App' +import reportWebVitals from './reportWebVitals' +console.log(process.env) + +ReactDOM.render( + + + , + document.getElementById('root') +) + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals() diff --git a/src/pages/AddressPage/AddressDetails.tsx b/src/pages/AddressPage/AddressDetails.tsx new file mode 100644 index 0000000..09326d1 --- /dev/null +++ b/src/pages/AddressPage/AddressDetails.tsx @@ -0,0 +1,275 @@ +import React from "react"; +import { Box, Text, Tip } from "grommet"; +import { + Address, + ExpandString, + formatNumber, + ONEValue, + TipContent, + TokenValue, +} from "src/components/ui"; +import { AddressDetails } from "src/types"; +import { TokensInfo } from "./TokenInfo"; +import { Erc20, useERC20Pool } from "src/hooks/ERC20_Pool"; +import { ONEValueDropdown } from "src/components/ui/OneValueDropdown"; +import { binanceAddressMap } from "src/config/BinanceAddressMap"; +import { useERC1155Pool } from "src/hooks/ERC1155_Pool"; +import { CircleQuestion } from "grommet-icons"; + +interface AddressDetailsProps { + address: string; + contracts: AddressDetails | null; + tokens: any[]; + balance?: string; +} + +export function AddressDetailsDisplay(props: AddressDetailsProps) { + const { address, contracts, tokens, balance } = props; + const erc20Map = useERC20Pool(); + const erc1155Map = useERC1155Pool(); + + const erc20Token = erc20Map[address] || null; + const type = getType(contracts, erc20Token); + const erc1151data = erc1155Map[address] || {}; + const { meta = {}, ...restErc1151data } = erc1151data; + + const data = { + ...contracts, + ...erc20Token, + token: tokens, + balance, + ...restErc1151data, + ...meta, + address, + }; + + if (!data) { + return null; + } + + const items: string[] = Object.keys(data); + + return ( + + {items.sort(sortByOrder).map((i) => ( + //@ts-ignore + + ))} + + ); +} + +export const Item = (props: { label: any; value: any }) => { + return ( + + + {props.label} + + + {props.value} + + + ); +}; + +function DetailItem(props: { data: any; name: string; type: TAddressType }) { + const { data, name, type } = props; + + if ( + !addressPropertyDisplayNames[name] || + !addressPropertyDisplayValues[name] || + data[name] === null + ) { + return null; + } + + return ( + + ); +} + +const addressPropertyDisplayNames: Record< + string, + (data: any, options: { type: TAddressType }) => React.ReactNode +> = { + address: () => { + return "Address"; + }, + value: () => "Value", + creatorAddress: () => "Creator", + // solidityVersion: () => "Solidity version", + meta: () => "Meta", + balance: () => "Balance", + // bytecode: () => "Bytecode", + token: () => "Token", + name: () => "Name", + symbol: () => "Symbol", + decimals: () => "Decimals", + totalSupply: () => "Total Supply", + holders: () => "Holders", + description: () => "Description", + transactionHash: () => "Transaction Hash", + circulating_supply: () => "Circulating Supply", +}; + +const addressPropertyDisplayValues: Record< + string, + (value: any, data: any, options: { type: TAddressType }) => React.ReactNode +> = { + address: (value, data, options: { type: TAddressType }) => { + return ( + <> +
+ {binanceAddressMap[value] ? ` (${binanceAddressMap[value]})` : null} + + ); + }, + value: (value) => , + creatorAddress: (value) =>
, + // solidityVersion: (value) => value, + IPFSHash: (value) => value, + meta: (value) => value, + // bytecode: (value) => , + balance: (value) => ( + + + + ), + token: (value) => , + name: (value) => value, + symbol: (value) => value, + decimals: (value) => value, + totalSupply: (value, data) => ( + + + + } + plain + > + + + + + + ), + holders: (value: string, data: any) => { + return ( + + <>{formatNumber(+value)} + + } + plain + > + + + + + + ); + }, + description: (value) => <>{value}, + transactionHash: (value) =>
, + circulating_supply: (value, data) => ( + + + + + } + plain + > + + + + + + + ), +}; + +function sortByOrder(a: string, b: string) { + return addressPropertyOrder[a] - addressPropertyOrder[b]; +} + +const addressPropertyOrder: Record = { + address: 9, + value: 10, + balance: 11, + token: 12, + transactionHash: 10, + creatorAddress: 13, + + name: 20, + symbol: 21, + decimals: 22, + totalSupply: 23, + circulating_supply: 23, + holders: 24, + + solidityVersion: 31, + IPFSHash: 32, + meta: 33, + bytecode: 34, + sourceCode: 34, +}; + +type TAddressType = "address" | "contract" | "erc20" | "erc721" | "erc1155"; + +export function getType( + contracts: AddressDetails | null, + erc20Token: Erc20 +): TAddressType { + if (!!contracts && !!erc20Token) { + return "erc20"; + } + + if (!!contracts) { + return "contract"; + } + + return "address"; +} diff --git a/src/pages/AddressPage/ContractDetails/AbiMethodView.tsx b/src/pages/AddressPage/ContractDetails/AbiMethodView.tsx new file mode 100644 index 0000000..69035e0 --- /dev/null +++ b/src/pages/AddressPage/ContractDetails/AbiMethodView.tsx @@ -0,0 +1,215 @@ +import { Box, Spinner, Text, TextInput } from "grommet"; +import React, { useEffect, useState } from "react"; +import styled from "styled-components"; +import { Button } from "src/components/ui/Button"; +import Web3 from "web3"; +import { AbiItem } from "web3-utils"; +import { convertInputs } from "./helpers"; + +const Field = styled(Box)``; + +const ViewWrapper = styled(Box)` + border: 1px solid #e7ecf7; + border-radius: 5px; +`; + +const NameWrapper = styled(Box)` + border-bottom: 1px solid #e7ecf7; + padding: 10px; + background: #f8f9fa; +`; + +const SmallTextInput = styled(TextInput)` + font-size: 14px; + font-weight: 400; + + ::placeholder { + font-size: 14px; + } +`; + +export const ActionButton = styled(Button)` + font-size: 14px; + padding: 7px 8px 5px 8px; + font-weight: 500; +`; + +const GreySpan = styled("span")` + font-size: 14px; + opacity: 0.7; + font-weight: 400; +`; + +const TextBold = styled(Text)` + font-weight: bold; +`; + +const GAS_LIMIT = 6721900; +const GAS_PRICE = 3000000000; + +export const AbiMethodsView = (props: { + abiMethod: AbiItem; + address: string; + metamaskAddress?: string; + index: number; +}) => { + const { abiMethod, address, index } = props; + const [inputsValue, setInputsValue] = useState( + [...new Array(abiMethod.inputs?.length)].map(() => "") + ); + const [amount, setAmount] = useState(""); + const [error, setError] = useState(""); + const [result, setResult] = useState(""); + const [loading, setLoading] = useState(false); + + const query = async () => { + try { + setError(""); + setResult(""); + setLoading(true); + + // @ts-ignore + const web3 = window.web3; + + const web3URL = web3 + ? web3.currentProvider + : process.env.REACT_APP_RPC_URL_SHARD0; + + const hmyWeb3 = new Web3(web3URL); + + const contract = new hmyWeb3.eth.Contract([abiMethod], address); + + if (abiMethod.name) { + let res; + + if (abiMethod.stateMutability === "view") { + res = await contract.methods[abiMethod.name] + .apply(contract, convertInputs(inputsValue, abiMethod.inputs || [])) + .call(); + } else { + // @ts-ignore + const accounts = await ethereum.enable(); + + const account = accounts[0] || web3.eth.defaultAccount; + + res = await contract.methods[abiMethod.name] + .apply(contract, convertInputs(inputsValue, abiMethod.inputs || [])) + .send({ + gasLimit: GAS_LIMIT, + gasPrice: GAS_PRICE, + from: account, + value: Number(amount) * 1e18, + }); + } + + setResult(typeof res === "object" ? res.status.toString() : res); + } + } catch (e) { + setError(e.message); + } + + setLoading(false); + }; + + useEffect(() => { + if ( + abiMethod.stateMutability !== "payable" && + (!abiMethod.inputs || !abiMethod.inputs.length) + ) { + query(); + } + }, []); + + const setInputValue = (value: string, idx: number) => { + const newArr = inputsValue.map((v, i) => (i === idx ? value : v)); + setInputsValue(newArr); + }; + + return ( + + + + {index + 1}. {abiMethod.name} + + + + + {abiMethod.stateMutability === "payable" ? ( + + + payableAmount ONE + + ) => + setAmount(evt.currentTarget.value) + } + /> + + ) : null} + {abiMethod.inputs && abiMethod.inputs.length ? ( + + {abiMethod.inputs.map((input, idx) => { + const name = input.name || ""; + + return ( + + + {name} ({input.type}) + + ) => + setInputValue(evt.currentTarget.value, idx) + } + /> + + ); + })} + + ) : null} + + {!result || abiMethod.inputs?.length ? ( + + {loading ? ( + + ) : abiMethod.stateMutability === "view" ? ( + Query + ) : ( + + Write + + )} + + ) : null} + + {abiMethod.outputs + ? abiMethod.outputs.map((input) => { + return ( + + {result ? ( + + {result} {input.type} + + ) : ( + + {"-> "} + {input.type} + + )} + + ); + }) + : null} + + {error && ( + + {error} + + )} + + + ); +}; diff --git a/src/pages/AddressPage/ContractDetails/ConnectWallets.tsx b/src/pages/AddressPage/ContractDetails/ConnectWallets.tsx new file mode 100644 index 0000000..7b507cc --- /dev/null +++ b/src/pages/AddressPage/ContractDetails/ConnectWallets.tsx @@ -0,0 +1,55 @@ +import { useCallback, useEffect, useState } from "react"; +import detectEthereumProvider from "@metamask/detect-provider"; +import { Box, Button, Text } from "grommet"; +import { ActionButton } from "./AbiMethodView"; + +export const Wallet = (params: { onSetMetamask: (v: string) => void }) => { + const [metamaskAddress, setMetamask] = useState(""); + + useEffect(() => { + params.onSetMetamask(metamaskAddress); + }, [metamaskAddress]); + + const signInMetamask = useCallback(() => { + detectEthereumProvider().then((provider: any) => { + try { + // @ts-ignore + if (provider !== window.ethereum) { + console.error("Do you have multiple wallets installed?"); + } + + if (!provider) { + alert("Metamask not found"); + } + + provider.on("accountsChanged", (accounts: string[]) => + setMetamask(accounts[0]) + ); + + provider.on("disconnect", () => { + setMetamask(""); + }); + + provider + .request({ method: "eth_requestAccounts" }) + .then(async (accounts: string[]) => { + setMetamask(accounts[0]); + }); + } catch (e) { + console.error(e); + } + }); + }, []); + + return ( + + {metamaskAddress ? ( + User address: {metamaskAddress} + ) : ( + + Sign in Metamask + + )} + + ); +}; diff --git a/src/pages/AddressPage/ContractDetails/helpers.ts b/src/pages/AddressPage/ContractDetails/helpers.ts new file mode 100644 index 0000000..c522091 --- /dev/null +++ b/src/pages/AddressPage/ContractDetails/helpers.ts @@ -0,0 +1,14 @@ +import { AbiInput } from "web3-utils"; +import { getAddress } from "src/utils"; + +export const convertInputs = (inputs: string[], abiInputs: AbiInput[]) => { + return inputs.map((value, idx) => { + switch (abiInputs[idx].type) { + case "address": + return getAddress(value).basicHex; + + default: + return value; + } + }); +}; diff --git a/src/pages/AddressPage/ContractDetails/index.tsx b/src/pages/AddressPage/ContractDetails/index.tsx new file mode 100644 index 0000000..3fc1a1b --- /dev/null +++ b/src/pages/AddressPage/ContractDetails/index.tsx @@ -0,0 +1,240 @@ +import React, { useState } from "react"; +import { Box, Text } from "grommet"; +import { AddressDetails } from "src/types"; +import { Item } from "../AddressDetails"; +import { useHistory } from "react-router-dom"; +import styled from "styled-components"; +import { ISourceCode } from "src/api/explorerV1"; +import { AbiMethodsView } from "./AbiMethodView"; +import { AbiItem } from "web3-utils"; +import { Wallet } from "./ConnectWallets"; + +const StyledTextArea = styled("textarea")` + padding: 0.75rem; + border: 1px solid #e7eaf3; + background-color: #f8f9fa; + border-radius: 0.35rem; +`; + +export const ContractDetails = (props: { + address: string; + contracts?: AddressDetails | null; + sourceCode?: ISourceCode | null; +}) => { + // console.log(111, appendABI(abi, props.address)); + + if (!!props.sourceCode) { + return ( + + ); + } + + if (!!props.contracts) { + return ; + } + + return null; +}; + +export const AbiMethods = (props: { + address: string; + abi: AbiItem[]; + metamaskAddress?: string; +}) => { + return ( + + {props.abi.map((abiMethod, idx) => + abiMethod.name ? ( + + ) : null + )} + + ); +}; + +export const NoVerifiedContractDetails = (props: { + contracts: AddressDetails; +}) => { + const history = useHistory(); + + return ( + + + + Are you the contract creator? + history.push(`/verifycontract`)} + color="brand" + > + Verify and Publish + {" "} + your contract source code today! + + + + + {props.contracts.IPFSHash ? ( + + ) : null} + + {props.contracts.bytecode || ""} + + } + /> + + + + ); +}; + +enum V_TABS { + CODE = "Code", + READ = "Read Contract", + WRITE = "Write Contract", +} + +const TabBox = styled(Box)` + border: 1px solid #dedede; + padding: 7px 12px 6px 12px; + border-radius: 4px; + margin: 5px 10px; +`; + +const TabButton = (props: { + text: string; + onClick: () => void; + selected: boolean; +}) => { + return ( + + + {props.text} + + + ); +}; + +export const VerifiedContractDetails = (props: { + sourceCode: ISourceCode; + address: string; + contracts?: AddressDetails | null; +}) => { + const [tab, setTab] = useState(V_TABS.CODE); + const [metamaskAddress, setMetamask] = useState(""); + + return ( + + + setTab(V_TABS.CODE)} + selected={tab === V_TABS.CODE} + /> + {props.sourceCode.abi ? ( + <> + setTab(V_TABS.READ)} + selected={tab === V_TABS.READ} + /> + setTab(V_TABS.WRITE)} + selected={tab === V_TABS.WRITE} + /> + + ) : null} + + {tab === V_TABS.CODE ? ( + + + + + + + + {props.sourceCode.sourceCode || ""} + + } + /> + {props.contracts ? ( + + {props.contracts.bytecode || ""} + + } + /> + ) : null} + + + + ) : null} + {tab === V_TABS.WRITE && props.sourceCode.abi ? ( + + + + a.stateMutability !== "view" && + !!a.name && + a.type === "function" + )} + address={props.address} + metamaskAddress={metamaskAddress} + /> + + ) : null} + + {tab === V_TABS.READ && props.sourceCode.abi ? ( + + a.stateMutability === "view" && a.type === "function" + )} + address={props.address} + /> + + ) : null} + + ); +}; diff --git a/src/pages/AddressPage/TokenInfo.tsx b/src/pages/AddressPage/TokenInfo.tsx new file mode 100644 index 0000000..7deba31 --- /dev/null +++ b/src/pages/AddressPage/TokenInfo.tsx @@ -0,0 +1,303 @@ +import React from "react"; +import { Box, Text, Tip } from "grommet"; +import { + Address, + formatNumber, + TipContent, + TokenValue, +} from "src/components/ui"; +import { Dropdown } from "src/components/dropdown/Dropdown"; +import { BinancePairs } from "src/hooks/BinancePairHistoricalPrice"; +import Big from "big.js"; +import { useERC20Pool } from "src/hooks/ERC20_Pool"; +import { useERC721Pool } from "src/hooks/ERC721_Pool"; +import { TokenValueBalanced } from "src/components/ui/TokenValueBalanced"; +import { useThemeMode } from "src/hooks/themeSwitcherHook"; +import { useCurrency } from "src/hooks/ONE-ETH-SwitcherHook"; +import { getAddress } from "src/utils/getAddress/GetAddress"; +import { useHistory } from "react-router-dom"; +import { useERC1155Pool } from "../../hooks/ERC1155_Pool"; +import { Alert } from "grommet-icons"; + +interface Token { + balance: string; + tokenAddress: string; + isERC20?: boolean; + isERC721?: boolean; + isERC1155?: boolean; + symbol: string; + tokenID?: string; +} + +export function TokensInfo(props: { value: Token[] }) { + const erc20Map = useERC20Pool(); + const erc721Map = useERC721Pool(); + const erc1155Map = useERC1155Pool(); + const themeMode = useThemeMode(); + const currency = useCurrency(); + const history = useHistory(); + + const { value } = props; + + if (!value.filter((i) => filterWithBalance(i.balance)).length) { + return ; + } + + const erc20Tokens = value + .filter((i) => filterWithBalance(i.balance)) + .filter((i) => i.isERC20) + .map((item) => ({ + ...item, + symbol: erc20Map[item.tokenAddress].symbol, + name: erc20Map[item.tokenAddress].name, + })) + .sort((a, b) => (a.name > b.name ? 1 : -1)); + + const erc721Tokens = value + .filter((i) => filterWithBalance(i.balance)) + .filter((i) => i.isERC721) + .map((item) => ({ + ...item, + symbol: erc721Map[item.tokenAddress].symbol, + name: erc721Map[item.tokenAddress].name, + })); + + const erc1155Tokens = value + .filter((i) => filterWithBalance(i.balance)) + .filter((i) => i.isERC1155) + .map((item) => ({ + ...item, + symbol: erc1155Map[item.tokenAddress].symbol, + name: erc1155Map[item.tokenAddress].name, + })); + + const data = [...erc20Tokens, ...erc721Tokens, ...erc1155Tokens]; + + return ( + + + + keyField={"tokenID"} + itemHeight={"55px"} + itemStyles={{ padding: "5px", marginBottom: "10px" }} + searchable={(item, searchText) => { + const outPutAddress = + currency === "ONE" + ? getAddress(item.tokenAddress).bech32 + : item.tokenAddress; + + searchText = searchText.toLowerCase(); + + if (item.tokenAddress.toLowerCase().includes(searchText)) { + return true; + } + + if (outPutAddress.toLowerCase().includes(searchText)) { + return true; + } + + if (item.symbol.toLowerCase().includes(searchText)) { + return true; + } + + return false; + }} + themeMode={themeMode} + items={data} + onClickItem={(item) => { + history.push(`/address/${item.tokenAddress}`); + }} + renderItem={(item) => { + console.log(item); + const symbol = + erc20Map[item.tokenAddress]?.symbol || + erc721Map[item.tokenAddress]?.symbol || + erc1155Map[item.tokenAddress]?.symbol; + + return ( + + + +
+ + ({symbol}) + + + + {item.isERC1155 && (item as any).needUpdate ? ( + } + plain + > + + + + + ) : null} + + {item.isERC1155 ? ( + + Token ID: {item.tokenID}{" "} + + ) : null} + + ); + }} + renderValue={() => ( + + {erc20Tokens.length ? ( + + HRC20{" "} + + {erc20Tokens.length} + + + ) : null} + {erc721Tokens.length ? ( + + HRC721{" "} + + {erc721Tokens.length} + + + ) : null} + {erc1155Tokens.length ? ( + + HRC1155{" "} + + {erc1155Tokens.length} + + + ) : null} + + )} + group={[ + { + groupBy: "isERC20", + renderGroupItem: () => ( + + HRC20 tokens + + ), + }, + { + groupBy: "isERC721", + renderGroupItem: () => ( + + HRC721 tokens + + ), + }, + { + groupBy: "isERC1155", + renderGroupItem: () => ( + + HRC1155 tokens + + ), + }, + ]} + /> + + + ); +} + +function TokenInfo(props: { value: Token }) { + const { value } = props; + + return ( + +
+ + + ); +} + +function filterWithBalance(balance: string) { + return !!+balance; +} diff --git a/src/pages/AddressPage/index.tsx b/src/pages/AddressPage/index.tsx new file mode 100644 index 0000000..29bdaf0 --- /dev/null +++ b/src/pages/AddressPage/index.tsx @@ -0,0 +1,309 @@ +import React, { useEffect, useState } from "react"; +import { Text, Tabs, Tab, Box } from "grommet"; +import { BasePage, BaseContainer } from "src/components/ui"; +import { AddressDetailsDisplay, getType } from "./AddressDetails"; +import { + getContractsByField, + getUserERC20Balances, + getUserERC721Assets, + getTokenERC721Assets, + getTokenERC1155Assets, + getUserERC1155Balances, +} from "src/api/client"; +import { useHistory, useParams } from "react-router-dom"; +import { useERC20Pool } from "src/hooks/ERC20_Pool"; +import { useERC721Pool } from "src/hooks/ERC721_Pool"; +import { useERC1155Pool } from "src/hooks/ERC1155_Pool"; +import { Transactions } from "./tabs/Transactions"; +import { + IUserERC721Assets, + TRelatedTransaction, +} from "src/api/client.interface"; +import { Inventory } from "./tabs/inventory/Inventory"; +import { getAllBalance, getBalance } from "src/api/rpc"; +import { ISourceCode, loadSourceCode } from "../../api/explorerV1"; +import { AddressDetails } from "../../types"; +import { ContractDetails } from "./ContractDetails"; +import { ERC1155Icon } from "src/components/ui/ERC1155Icon"; +import { getAddress } from "src/utils"; +import { useCurrency } from "src/hooks/ONE-ETH-SwitcherHook"; + +export function AddressPage() { + const history = useHistory(); + const tabParamName = "activeTab="; + let activeTab = 0; + try { + activeTab = +history.location.search.slice( + history.location.search.indexOf("activeTab=") + tabParamName.length + ); + } catch { + activeTab = 0; + } + + const [contracts, setContracts] = useState(null); + const [sourceCode, setSourceCode] = useState(null); + const [balance, setBalance] = useState([]); + const [tokens, setTokens] = useState(null); + const [inventory, setInventory] = useState([]); + const [activeIndex, setActiveIndex] = useState(+activeTab); + const erc20Map = useERC20Pool(); + const erc721Map = useERC721Pool(); + const erc1155Map = useERC1155Pool(); + const currency = useCurrency(); + + //TODO remove hardcode + // @ts-ignore + const { id } = useParams(); + const erc20Token = erc20Map[id] || null; + let oneAddress = id; + + let type = erc721Map[id] + ? "erc721" + : erc1155Map[id] + ? "erc1155" + : getType(contracts, erc20Token); + + try { + oneAddress = getAddress(oneAddress).bech32; + } catch { + oneAddress = oneAddress; + } + + useEffect(() => { + const getActiveIndex = () => { + setActiveIndex(activeTab || 0); + }; + getActiveIndex(); + }, [id]); + + useEffect(() => { + const getBal = async () => { + let bal: string[] = []; + try { + bal = await getAllBalance([id, "latest"]); + } catch { + bal = []; + } + + setBalance(bal); + }; + getBal(); + }, [id]); + + useEffect(() => { + // if (!!contracts) { + loadSourceCode(oneAddress) + .then(setSourceCode) + .catch(() => setSourceCode(null)); + // } + }, [oneAddress]); + + useEffect(() => { + const getContracts = async () => { + try { + let contracts: any = await getContractsByField([0, "address", id]); + + const mergedContracts: AddressDetails = erc721Map[contracts.address] + ? { ...contracts, ...erc721Map[contracts.address] } + : contracts; + + setContracts(mergedContracts); + } catch (err) { + setContracts(null); + } + }; + getContracts(); + }, [id]); + + useEffect(() => { + const getInventory = async () => { + try { + if (type === "erc721" || type === "erc1155") { + let inventory = + type === "erc721" + ? await getTokenERC721Assets([id]) + : await (await getTokenERC1155Assets([id])).map((item) => { + if (item.meta && item.meta.image) { + item.meta.image = `${process.env.REACT_APP_INDEXER_IPFS_GATEWAY}${item.meta.image}`; + } + return item; + }); + + setInventory( + inventory + .filter((item) => item.meta) + .map((item) => { + item.type = type; + return item; + }) + ); + } else { + setInventory([]); + } + } catch (err) { + setInventory([]); + } + }; + getInventory(); + }, [id, erc721Map]); + + useEffect(() => { + const getTokens = async () => { + try { + let erc721Tokens = await getUserERC721Assets([id]); + let tokens = await getUserERC20Balances([id]); + let erc1155tokens = await getUserERC1155Balances([id]); + + const erc721BalanceMap = erc721Tokens.reduce((prev, cur) => { + if (prev[cur.tokenAddress]) { + prev[cur.tokenAddress]++; + } else { + prev[cur.tokenAddress] = 1; + } + + return prev; + }, {} as { [token: string]: number }); + + setTokens([ + ...tokens.map((token) => ({ ...token, isERC20: true })), + ...erc721Tokens.map((token) => ({ + ...token, + balance: erc721BalanceMap[token.tokenAddress].toString(), + isERC721: true, + })), + ...erc1155tokens.map((item) => ({ + ...item, + balance: item.amount, + isERC1155: true, + })), + ]); + } catch (err) { + setTokens(null); + } + }; + getTokens(); + }, [id]); + + const renderTitle = () => { + const erc1155 = erc1155Map[id] || {}; + const { meta = {}, ...restErc1155 } = erc1155; + const data = { + ...contracts, + ...erc20Token, + address: id, + token: tokens, + ...meta, + }; + + if (type === "erc20") { + return `HRC20 ${data.name}`; + } + + if (type === "erc721") { + return `ERC721 ${data.name}`; + } + + if (type === "erc1155") { + const title = `HRC1155 ${data.name || ""}`; + return meta.image ? ( + + +   + {title} + + ) : ( + title + ); + } + + if (type === "contract") { + return "Contract"; + } + + return "Address"; + }; + + const tabs: TRelatedTransaction[] = [ + "transaction", + "staking_transaction", + "internal_transaction", + "erc20", + "erc721", + "erc1155", + ]; + + return ( + + + {renderTitle()} + + + + + + { + history.replace( + `${history.location.pathname}?activeTab=${newActive}` + ); + setActiveIndex(newActive); + }} + > + Transactions}> + + + + Staking}> + + + + Internal}> + + + + HRC20 Transfers}> + + + + NFT Transfers}> + + + + {(type === "erc721" || type === "erc1155") && inventory.length ? ( + Inventory ({inventory.length})} + > + + + ) : null} + + {!!contracts || !!sourceCode ? ( + Contract}> + + + ) : null} + + {/*{type === "erc1155" && inventory.length ? (*/} + {/* Inventory ({inventory.length})}*/} + {/* >*/} + {/* */} + {/* */} + {/*) : null}*/} + + + + ); +} diff --git a/src/pages/AddressPage/tabs/Transactions.tsx b/src/pages/AddressPage/tabs/Transactions.tsx new file mode 100644 index 0000000..d1d20d5 --- /dev/null +++ b/src/pages/AddressPage/tabs/Transactions.tsx @@ -0,0 +1,483 @@ +import React, { useEffect, useState } from "react"; +import { Box, ColumnConfig, Text } from "grommet"; +import { FormNextLink } from "grommet-icons"; +import { useParams } from "react-router-dom"; +import { + getByteCodeSignatureByHash, + getRelatedTransactions, + getRelatedTransactionsByType, +} from "src/api/client"; +import { TransactionsTable } from "src/components/tables/TransactionsTable"; +import { + Address, + CalculateFee, + ONEValue, + RelativeTimer, +} from "src/components/ui"; +import { + Filter, + RelatedTransaction, + RelatedTransactionType, + RPCTransaction, +} from "src/types"; +import styled, { css } from "styled-components"; +import { TRelatedTransaction } from "src/api/client.interface"; +import { getERC20Columns } from "./erc20Columns"; + +const initFilter: Filter = { + offset: 0, + limit: 10, + orderBy: "block_number", + orderDirection: "desc", + filters: [{ type: "gte", property: "block_number", value: 0 }], +}; + +const Marker = styled.div<{ out: boolean }>` + border-radius: 2px; + padding: 5px; + + text-align: center; + font-weight: bold; + + ${(props) => + props.out + ? css` + background: rgb(239 145 62); + color: #fff; + ` + : css` + background: rgba(105, 250, 189, 0.8); + color: #1b295e; + `}; +`; + +const NeutralMarker = styled(Box)` + border-radius: 2px; + padding: 5px; + + text-align: center; + font-weight: bold; +`; + +function getColumns(id: string): ColumnConfig[] { + return [ + // { + // property: "type", + // size: "", + // header: ( + // + // Type + // + // ), + // render: (data: RelatedTransaction) => ( + // + // {relatedTxMap[data.transactionType] || data.transactionType} + // + // ), + // }, + { + property: "hash", + header: ( + + Hash + + ), + render: (data: any) => ( +
+ ), + }, + { + property: "method", + header: ( + + Method + + ), + render: (data: any) => { + let signature; + + try { + // @ts-ignore + signature = + data.signatures && + data.signatures.map((s: any) => s.signature)[0].split("(")[0]; + } catch (err) {} + + if (!signature && data.value !== "0") { + signature = "transfer"; + } + + if (!signature && data.input.length >= 10) { + signature = data.input.slice(2, 10); + } + + if (!signature) { + return {"—"}; + } + + return ( + + + {signature} + + + ); + }, + }, + // { + // property: "shard", + // header: ( + // + // Shard + // + // ), + // render: (data: RelatedTransaction) => ( + // + // {0} + // + // {0} + // + // ), + // }, + { + property: "from", + header: ( + + From + + ), + render: (data: RelatedTransaction) => ( + +
+ + ), + }, + { + property: "marker", + header: <>, + render: (data: RelatedTransaction) => ( + + + {data.from === id ? "OUT" : "IN"} + + + ), + }, + { + property: "to", + header: ( + + To + + ), + render: (data: RelatedTransaction) => ( + +
+ + ), + }, + { + property: "value", + header: ( + + Value + + ), + render: (data: RelatedTransaction) => ( + + + + ), + }, + + { + property: "timestamp", + header: ( + + Timestamp + + ), + render: (data: RelatedTransaction) => ( + + + + ), + }, + ]; +} + +const getStackingColumns = (id: string): ColumnConfig[] => { + return [ + { + property: "hash", + header: ( + + Hash + + ), + render: (data: any) => ( +
+ ), + }, + { + property: "type", + header: ( + + Type + + ), + render: (data: RelatedTransaction) => ( + + {data.type} + + ), + }, + { + property: "validator", + header: ( + + Validator + + ), + render: (data: RelatedTransaction) => ( + + {data.msg?.validatorAddress ? ( +
+ ) : ( + "-" + )} + + ), + }, + { + property: "marker", + header: <>, + render: (data: RelatedTransaction) => ( + + + {data.from === id ? "OUT" : "IN"} + + + ), + }, + { + property: "delegator", + header: ( + + Delegator + + ), + render: (data: RelatedTransaction) => ( + + {data.msg?.delegatorAddress ? ( +
+ ) : ( + "-" + )} + + ), + }, + { + property: "value", + header: ( + + Value + + ), + render: (data: RelatedTransaction) => ( + + {data.msg?.amount ? ( + + ) : ( + "-" + )} + + ), + }, + { + property: "timestamp", + header: ( + + Timestamp + + ), + render: (data: RelatedTransaction) => ( + + + + ), + }, + ]; +}; + +const relatedTxMap: Record = { + transaction: "Transaction", + internal_transaction: "Internal Transaction", + stacking_transaction: "Staking Transaction", +}; + +export function Transactions(props: { + type: TRelatedTransaction; + rowDetails?: (row: any) => JSX.Element; +}) { + const [relatedTrxs, setRelatedTrxs] = useState([]); + const [filter, setFilter] = useState<{ [name: string]: Filter }>({ + transaction: { ...initFilter }, + staking_transaction: { ...initFilter }, + internal_transaction: { ...initFilter }, + erc20: { ...initFilter }, + erc721: { ...initFilter }, + erc1155: { ...initFilter }, + }); + const [isLoading, setIsLoading] = useState(false); + + const { limit = 10 } = filter[props.type]; + + // @ts-ignore + const { id } = useParams(); + + useEffect(() => { + const getElements = async () => { + setIsLoading(true); + try { + let relatedTransactions = await getRelatedTransactionsByType([ + 0, + id, + props.type, + filter[props.type], + ]); + + // for transactions we display call method if any + if (props.type === "transaction") { + const methodSignatures = await Promise.all( + relatedTransactions.map((tx: any) => { + return tx.input && tx.input.length > 10 + ? getByteCodeSignatureByHash([tx.input.slice(0, 10)]) + : Promise.resolve([]); + }) + ); + + relatedTransactions = relatedTransactions.map((l, i) => ({ + ...l, + signatures: methodSignatures[i], + })); + } + + relatedTransactions = relatedTransactions.map((tx: any) => { + tx.relatedAddress = id; + return tx; + }); + + setIsLoading(false); + setRelatedTrxs(relatedTransactions); + } catch (err) { + console.log(err); + } + }; + getElements(); + }, [filter[props.type], id, props.type]); + + let columns = []; + + switch (props.type) { + case "staking_transaction": { + columns = getStackingColumns(id); + break; + } + case "erc20": { + columns = getERC20Columns(id); + break; + } + + default: { + columns = getColumns(id); + break; + } + } + + return ( + + setFilter({ ...filter, [props.type]: value })} + noScrollTop + minWidth="1266px" + hideCounter + rowDetails={props.rowDetails} + /> + + ); +} diff --git a/src/pages/AddressPage/tabs/erc20Columns.tsx b/src/pages/AddressPage/tabs/erc20Columns.tsx new file mode 100644 index 0000000..0dc1d5e --- /dev/null +++ b/src/pages/AddressPage/tabs/erc20Columns.tsx @@ -0,0 +1,257 @@ +import { Box, ColumnConfig, Text } from 'grommet' +import { Address, TokenValue, RelativeTimer } from 'src/components/ui' +import { RelatedTransaction } from 'src/types' +import React from 'react' +import { parseSuggestedEvent } from 'src/web3/parseByteCode' +import styled, { css } from 'styled-components' +import { zeroAddress } from 'src/utils/zeroAddress' + +const erc20TransferTopic = + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' + + +const memo = (f: Function) => { + const cache = new Map() + + return (data: any) => { + const hash: string = data.hash + (data.logs ? data.logs.length : '') + if (cache.has(hash)) { + return cache.get(hash) + } + + const res = f(data) + const { parsed } = res + if (!parsed) { + return res + } + + cache.set(hash, res) + return res + } +} + +// take only first related at the moment +const extractTransfer = memo((data: any) => { + const { relatedAddress } = data + const transferLogs = data.logs ? data.logs + .filter((d: any) => d.topics.includes(erc20TransferTopic)) : [] + + for (let i = 0; i < transferLogs.length; i++) { + const transferLog = transferLogs[i] + const event = parseSuggestedEvent('Transfer(address,address,uint256)', transferLog.data, transferLog.topics) || null + + if (!event) { + continue + } + + event.parsed['$0'] = event.parsed['$0'].toLowerCase() + event.parsed['$1'] = event.parsed['$1'].toLowerCase() + + if (relatedAddress === event.parsed['$0'] || relatedAddress === event.parsed['$1']) { + return { + transferLog: transferLog || {}, + parsed: event.parsed || {} + } + } + } + + return { + transferLog: {}, + parsed: {} + } +}) + +const Marker = styled.div<{ out: boolean }>` + border-radius: 2px; + padding: 5px; + + text-align: center; + font-weight: bold; + + ${(props) => + props.out + ? css` + background: rgb(239 145 62); + color: #fff; + ` + : css` + background: rgba(105, 250, 189, 0.8); + color: #1b295e; + `}; +` + +const NeutralMarker = styled(Box)` + border-radius: 2px; + padding: 5px; + + text-align: center; + font-weight: bold; +` + +export function getERC20Columns(id: string): ColumnConfig[] { + return [ + { + property: 'hash', + header: ( + + Hash + + ), + render: (data: any) => ( +
+ ) + }, + { + property: 'event', + header: ( + + Event + + ), + render: (data: any) => { + const { parsed } = extractTransfer(data) + const address1 = parsed['$0'] + const address2 = parsed['$1'] + const method = address1 === zeroAddress ? 'Mint' : address2 === zeroAddress ? 'Burn' : 'Transfer' + + return ( + + + {method} + + + ) + } + }, + { + property: 'from', + header: ( + + From + + ), + render: (data: RelatedTransaction) => { + const { parsed } = extractTransfer(data) + const address = (parsed['$0'] || '') || '?' + + return ( + +
+ ) + } + }, + { + property: 'marker', + header: <>, + render: (data: RelatedTransaction) => { + const { parsed } = extractTransfer(data) + const address = (parsed['$0'] || '') || '?' + + return ( + + + {address === id ? 'OUT' : 'IN'} + + + ) + } + }, + { + property: 'to', + header: ( + + To + + ), + render: (data: RelatedTransaction) => { + const { parsed } = extractTransfer(data) + const address = (parsed['$1'] || '') || '?' + + return ( + +
+ ) + } + }, + { + property: 'value', + header: ( + + Value + + ), + render: (data: RelatedTransaction) => { + const { parsed, transferLog } = extractTransfer(data) + const value = parsed['$2'] + + if (!transferLog || !value) { + return '?' + } + + return ( + + + ) + } + }, + { + property: 'token', + header: ( + + Token + + ), + render: (data: any) => { + const { transferLog } = extractTransfer(data) + const address = transferLog ? transferLog.address : '—' + + return ( + +
+ + ) + } + }, + { + property: 'timestamp', + header: ( + + Timestamp + + ), + render: (data: RelatedTransaction) => ( + + + + ) + } + ] +} diff --git a/src/pages/AddressPage/tabs/inventory/Inventory.tsx b/src/pages/AddressPage/tabs/inventory/Inventory.tsx new file mode 100644 index 0000000..376e564 --- /dev/null +++ b/src/pages/AddressPage/tabs/inventory/Inventory.tsx @@ -0,0 +1,42 @@ +import React, { useState } from "react"; +import { Box } from "grommet"; + +import { IUserERC721Assets } from "src/api/client.interface"; +import { InventoryItem } from "./InventoryItem"; +import { Pagination } from "src/components/pagination/Pagination"; + +export interface IInventoryProps { + inventory: IUserERC721Assets[]; +} + +export function Inventory(props: IInventoryProps) { + const [page, setPage] = useState(0); + + const { inventory } = props; + const pageSize = 10; + const maxPage = Math.ceil(inventory.length / pageSize); + const renderedInventory = inventory.slice( + page * pageSize, + (page + 1) * pageSize + ); + + return ( + + + setPage(page - 1)} + onClickNext={() => { + setPage(page + 1); + }} + /> + + + {renderedInventory.map((item) => { + return ; + })} + + + ); +} diff --git a/src/pages/AddressPage/tabs/inventory/InventoryItem.tsx b/src/pages/AddressPage/tabs/inventory/InventoryItem.tsx new file mode 100644 index 0000000..b8ca8e3 --- /dev/null +++ b/src/pages/AddressPage/tabs/inventory/InventoryItem.tsx @@ -0,0 +1,142 @@ +import React, { useState } from "react"; + +import styled from "styled-components"; + +import { IUserERC721Assets } from "src/api/client.interface"; +import { Box, Spinner, Text } from "grommet"; +import { Address } from "src/components/ui"; +import { Alert } from "grommet-icons"; +import { useHistory } from "react-router-dom"; + +export interface IInventoryItemProps { + item: IUserERC721Assets; +} + +const InventItem = styled.div` + width: 215px; + height: 270px; + position: relative; + margin: 10px; +`; + +const Loader = styled.div` + position: absolute; + width: 215px; + height: 270px; + background: ${(props) => props.theme.backgroundBack}; +`; + +const InventImg = styled.img` + width: 100%; +`; + +const ErrorPreview = styled(Box)` + width: 215px; + height: 270px; + + border-radius: 8px; +`; + +const EmptyImage = styled(Box)` + width: 215px; + height: 270px; + + border-radius: 8px; +`; + +const Image = styled(Box)` + width: 30px; + height: 30px; + + border-radius: 8px; + background: ${(props) => props.theme.global.colors.backgroundEmptyIcon}; +`; + +export function InventoryItem(props: IInventoryItemProps) { + const [isLoading, setIsLoading] = useState(!!props.item?.meta?.image); + const [isErrorLoading, setIsErrorLoading] = useState(false); + const history = useHistory(); + + const url = props.item?.meta?.image || ""; + const description = props.item?.meta?.description || ""; + const { tokenID, ownerAddress } = props.item; + return ( + + history.push( + `/inventory/${props.item.type}/${props.item.tokenAddress}/${props.item.tokenID}` + ) + } + style={{ cursor: "pointer" }} + > + + {isLoading ? ( + + + + + + ) : null} + + + {isErrorLoading ? ( + + + No Image + + ) : url ? ( + setIsLoading(false)} + onError={() => { + setIsLoading(false); + setIsErrorLoading(true); + }} + /> + ) : ( + + + No image + + )} + + + + + # + {tokenID.length > 8 + ? `${tokenID.slice(0, 5)}...${tokenID.slice(-5)}` + : tokenID} + + {ownerAddress ? ( + + + Owner + {" "} +
+ + ) : null} + + {" "} + + ); +} diff --git a/src/pages/AllBlocksPage/AllBlocksTable.tsx b/src/pages/AllBlocksPage/AllBlocksTable.tsx new file mode 100644 index 0000000..1af6420 --- /dev/null +++ b/src/pages/AllBlocksPage/AllBlocksTable.tsx @@ -0,0 +1,208 @@ +import React, { useEffect, useState } from "react"; +import dayjs from "dayjs"; + +import { Box, DataTable, Text, Spinner } from "grommet"; +import { Block, Filter } from "src/types"; +import { useHistory, useParams } from "react-router-dom"; +import { + Address, + formatNumber, + RelativeTimer, + PaginationBlockNavigator, + PaginationRecordsPerPage, +} from "src/components/ui"; +import { getBlocks, getCount } from "src/api/client"; +import { ShardDropdown } from "src/components/ui/ShardDropdown"; + +function getColumns(props: any) { + const { history, shardNumber } = props; + return [ + { + property: "shard", + header: ( + + Shard + + ), + render: (data: Block) => {shardNumber}, + }, + { + property: "number", + header: ( + + Height + + ), + render: (data: Block) => ( + { + history.push(`/block/${data.hash}`); + }} + color="brand" + > + {formatNumber(+data.number)} + + ), + }, + { + property: "timestamp", + header: ( + + Timestamp + + ), + render: (data: Block) => ( + + {/**/} + {/* {dayjs(data.timestamp).format("YYYY-MM-DD, HH:mm:ss")},*/} + {/**/} + + + ), + }, + { + property: "miner", + primaryKey: true, + header: ( + + Miner + + ), + render: (data: Block) =>
, + }, + { + property: "transactions", + header: ( + + Transactions + + ), + render: (data: Block) => ( + + {data.transactions.length + data.stakingTransactions.length} + + ), + }, + { + property: "gasUsed", + header: ( + + Gas Used / Gas Limit + + ), + render: (data: Block) => ( + + {formatNumber(+data.gasUsed)} / {formatNumber(+data.gasLimit)} + + ), + }, + ]; +} + +const initFilter: Filter = { + offset: 0, + limit: 10, + orderBy: "number", + orderDirection: "desc", + filters: [{ type: "gte", property: "number", value: 0 }], +}; + +export function AllBlocksTable() { + const [blocks, setBlocks] = useState([]); + const [filter, setFilter] = useState(initFilter); + + // @ts-ignore + const { shardNumber } = useParams(); + + const history = useHistory(); + + useEffect(() => { + const newFilter = JSON.parse(JSON.stringify(initFilter)) as Filter; + + setFilter(newFilter); + }, [shardNumber]); + + useEffect(() => { + const getElements = async () => { + try { + let blocks = await getBlocks([+shardNumber, filter]); + setBlocks(blocks as Block[]); + } catch (err) { + console.log(err); + } + }; + getElements(); + }, [filter]); + + if (!blocks.length) { + return ( + + + + ); + } + + const beginValue = blocks[0].number; + const endValue = blocks.slice(-1)[0].number; + + return ( + <> + + + {filter.limit} blocks shown, from{" "} + #{formatNumber(+endValue)} to{" "} + #{formatNumber(+beginValue)} + + + + + + + + + + + + ); +} diff --git a/src/pages/AllBlocksPage/index.tsx b/src/pages/AllBlocksPage/index.tsx new file mode 100644 index 0000000..c4db4ab --- /dev/null +++ b/src/pages/AllBlocksPage/index.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { Box, Heading, Text } from "grommet"; +import { BasePage, BaseContainer } from "src/components/ui"; +import { AllBlocksTable } from "./AllBlocksTable"; +import { useHistory, useParams } from "react-router-dom"; +import { ShardDropdown } from "src/components/ui/ShardDropdown"; + +export function AllBlocksPage() { + // @ts-ignore + const { shardNumber } = useParams(); + + const history = useHistory(); + + return ( + + + Blocks + + + + Filter: + + history.push(`/blocks/shard/${shardNumber}`) + } + /> + + + + + + + ); +} diff --git a/src/pages/AllTransactionsPage/index.tsx b/src/pages/AllTransactionsPage/index.tsx new file mode 100644 index 0000000..a866bae --- /dev/null +++ b/src/pages/AllTransactionsPage/index.tsx @@ -0,0 +1,95 @@ +import React, { useEffect, useState } from "react"; +import { Box, Heading, Text } from "grommet"; +import { BasePage, BaseContainer } from "src/components/ui"; +import { TransactionsTable } from "../../components/tables/TransactionsTable"; +import { Filter, RPCTransactionHarmony } from "../../types"; +import { useHistory } from "react-router"; +import { getTransactions, getCount } from "src/api/client"; +import { ShardDropdown } from "src/components/ui/ShardDropdown"; +import { useParams } from "react-router-dom"; + +const initFilter: Filter = { + offset: 0, + limit: 10, + orderBy: "block_number", + orderDirection: "desc", + filters: [{ type: "gte", property: "block_number", value: 0 }], +}; + +export function AllTransactionsPage() { + const [trxs, setTrxs] = useState([]); + const [count, setCount] = useState(""); + const [filter, setFilter] = useState(initFilter); + + // @ts-ignore + const { shardNumber } = useParams(); + + const history = useHistory(); + + useEffect(() => { + const getRes = async () => { + try { + let res = await getCount([+shardNumber, "transactions"]); + setCount(res.count); + } catch (err) { + console.log(err); + } + }; + + getRes().then(() => { + const newFilter = JSON.parse(JSON.stringify(filter)) as Filter; + const innerFilter = newFilter.filters.find( + (i) => i.property === "block_number" + ); + if (innerFilter && count) { + innerFilter.value = +count; + } + + setFilter(newFilter); + }); + }, [shardNumber]); + + useEffect(() => { + const getElements = async () => { + try { + let trxs = await getTransactions([+shardNumber, filter]); + + setTrxs(trxs as RPCTransactionHarmony[]); + } catch (err) { + console.log(err); + } + }; + getElements(); + }, [filter, shardNumber]); + + const { limit = 10 } = filter; + + return ( + + + Transactions + + + + Filter: + + history.push(`/transactions/shard/${shardNumber}`) + } + /> + + + + + + + ); +} diff --git a/src/pages/BlockPage.tsx b/src/pages/BlockPage.tsx new file mode 100644 index 0000000..14c78c4 --- /dev/null +++ b/src/pages/BlockPage.tsx @@ -0,0 +1,100 @@ +import { BlockDetails } from "src/components/block/BlockDetails"; +import { Block } from "../types"; +import { useParams } from "react-router-dom"; +import { BasePage } from "src/components/ui"; +import { getBlockByNumber, getBlockByHash } from "src/api/client"; + +import React, { useEffect, useState } from "react"; +import { Heading } from "grommet"; + +export const BlockPage = () => { + // hash or number + // @ts-ignore + const { id } = useParams(); + const [block, setBlock] = useState(null); + const [blockNumber, setBlockNumber] = useState(0); + + const availableShards = (process.env.REACT_APP_AVAILABLE_SHARDS as string) + .split(",") + .map((t) => +t); + + useEffect(() => { + let cleanupFunction = false; + + const exec = async () => { + let block; + if ("" + +id === id) { + try { + block = await getBlockByNumber([0, +id]); + setBlockNumber(0); + } catch { + try { + if (!block && availableShards.find((i) => i === 1)) { + block = await getBlockByNumber([1, +id]); + setBlockNumber(1); + } + } catch { + try { + if (!block && availableShards.find((i) => i === 2)) { + block = await getBlockByNumber([2, +id]); + setBlockNumber(2); + } + } catch { + if (!block && availableShards.find((i) => i === 3)) { + block = await getBlockByNumber([3, +id]); + setBlockNumber(3); + } + } + } + } + } else { + try { + block = await getBlockByHash([0, id]); + setBlockNumber(0); + } catch { + try { + if (!block && availableShards.find((i) => i === 1)) { + block = await getBlockByHash([1, id]); + setBlockNumber(1); + } + } catch { + try { + if (!block && availableShards.find((i) => i === 2)) { + block = await getBlockByHash([2, id]); + setBlockNumber(2); + } + } catch { + if (!block && availableShards.find((i) => i === 3)) { + block = await getBlockByHash([3, id]); + setBlockNumber(3); + } + } + } + } + } + if (!cleanupFunction) { + setBlock(block as Block); + } + }; + exec(); + + return () => { + cleanupFunction = true; + }; + }, [id]); + + if (!block) { + return null; + } + + return ( + <> + + Block #{block.number} + + + + + + ); +}; diff --git a/src/pages/ERC1155List/ERC1155Table.tsx b/src/pages/ERC1155List/ERC1155Table.tsx new file mode 100644 index 0000000..a379afd --- /dev/null +++ b/src/pages/ERC1155List/ERC1155Table.tsx @@ -0,0 +1,214 @@ +import React from "react"; + +import { Box, DataTable, Text, Spinner, Tip } from "grommet"; +import { Filter } from "src/types"; +import { useHistory } from "react-router-dom"; +import { + Address, + formatNumber, + PaginationNavigator, + PaginationRecordsPerPage, + TipContent, + TokenValue, + TPaginationAction, +} from "src/components/ui"; +import { Erc20 } from "../../hooks/ERC20_Pool"; +import { ERC1155Icon } from "src/components/ui/ERC1155Icon"; +import { CircleQuestion } from "grommet-icons"; + +interface TransactionTableProps { + data: any[]; + totalElements: number; + limit: number; + filter: Filter; + setFilter: (filter: Filter, action?: TPaginationAction) => void; + showIfEmpty?: boolean; + emptyText?: string; + isLoading?: boolean; + minWidth?: string; +} + +export function ERC1155Table(props: TransactionTableProps) { + const history = useHistory(); + const { + data, + totalElements, + limit, + filter, + setFilter, + emptyText = "No data to display", + isLoading, + minWidth = "1310px", + } = props; + + if (isLoading) { + return ( + + + + ); + } + + if (!data.length) { + return ( + + {emptyText} + + ); + } + + return ( + <> + + + {Math.min(limit, data.length)} token + {data.length !== 1 ? "s" : ""} shown + + + + + + + + + + + + ); +} + +function getColumns(props: any) { + return [ + { + property: "icon", + resizeable: false, + size: "xxsmall", + header: ( + + ), + render: (data: Erc20) => { + return ; + }, + }, + { + property: "name", + resizeable: false, + header: ( + + Name + + ), + render: (data: Erc20) => {data.name}, + }, + { + property: "symbol", + size: "xsmall", + resizeable: false, + header: ( + + Symbol + + ), + render: (data: Erc20) => {data.symbol}, + }, + { + property: "address", + primary: true, + header: ( + + Address + + ), + render: (data: Erc20) =>
, + }, + // { + // property: "totalSupply", + // size: 'small', + // resizeable: false, + // header: ( + // + // Total supply + // + // ), + // render: (data: Erc20) => { + // return ; + // }, + // }, + { + property: "holders", + size: "small", + resizeable: false, + header: ( + + Holders + + ), + render: (data: Erc20) => ( + + + {formatNumber(+data.holders)} + + + } + plain + > + + + + + + ), + }, + ]; +} diff --git a/src/pages/ERC1155List/index.tsx b/src/pages/ERC1155List/index.tsx new file mode 100644 index 0000000..963da8d --- /dev/null +++ b/src/pages/ERC1155List/index.tsx @@ -0,0 +1,127 @@ +import { BasePage, TPaginationAction } from "src/components/ui"; + +import React, { useEffect, useState } from "react"; +import { Box, Heading, Spinner, Text, TextInput } from "grommet"; +import { Filter } from "src/types"; +import { useThemeMode } from "src/hooks/themeSwitcherHook"; +import { ERC1155Table } from "./ERC1155Table"; +import { Search } from "grommet-icons"; +import { useERC1155Pool, ERC1155 } from "src/hooks/ERC1155_Pool"; + +const initFilter: Filter = { + offset: 0, + limit: 10, + orderBy: "block_number", + orderDirection: "desc", + filters: [{ type: "gte", property: "block_number", value: 0 }], +}; + +export const ERC1155List = () => { + const [data, setData] = useState([]); + const [filter, setFilter] = useState(initFilter); + const [search, setSearch] = useState(""); + const erc1155 = useERC1155Pool(); + const themeMode = useThemeMode(); + const erc1155Tokens = Object.values(erc1155); + + const searchedTokenLength = erc1155Tokens.filter( + filterWithFields(["name", "symbol"], search) + ).length; + + useEffect(() => { + setData( + erc1155Tokens + .filter(filterWithFields(["name", "symbol"], search)) + .sort(sortWithHolders) + //@ts-ignore + .slice(filter.offset, filter.offset + filter.limit) + ); + }, [erc1155, filter, search]); + + useEffect(() => { + setFilter({ ...filter, offset: 0 }); + }, [search]); + + const onChangeFilter = (newFilter: Filter, action?: TPaginationAction) => { + //@ts-ignore + if (action === "prevPage" && filter.offset > 0) { + //@ts-ignore + newFilter.offset = Math.max(0, filter.offset - filter.limit); + } + + if ( + action === "nextPage" && + //@ts-ignore + filter.offset + filter.limit < + (!!search ? searchedTokenLength : erc1155Tokens.length) + ) { + newFilter.offset = Math.min( + //@ts-ignore + erc1155Tokens.length, + //@ts-ignore + filter.offset + filter.limit + ); + } + + setFilter(newFilter); + }; + + const { limit = 10 } = filter; + + return ( + <> + + HRC1155 Tokens + + + + setSearch(e.target.value)} + color="red" + icon={} + style={{ + backgroundColor: themeMode === "light" ? "white" : "transparent", + fontWeight: 500, + }} + placeholder="Search by Name / Symbol" + /> + + {!erc1155Tokens.length && !search && ( + + + + )} + {!erc1155Tokens.length && search && ( + + No tokens for this search + + )} + {!!erc1155Tokens.length && ( + + )} + + + ); +}; + +function filterWithFields(fields: Array, search: string) { + return (erc721: ERC1155) => { + return fields.some((field) => + erc721[field].toString().toLowerCase().includes(search.toLowerCase()) + ); + }; +} + +function sortWithHolders(a: ERC1155, b: ERC1155) { + return +b.holders - +a.holders; +} diff --git a/src/pages/ERC20List/ERC20Table.tsx b/src/pages/ERC20List/ERC20Table.tsx new file mode 100644 index 0000000..fbc6387 --- /dev/null +++ b/src/pages/ERC20List/ERC20Table.tsx @@ -0,0 +1,261 @@ +import React from "react"; + +import { Box, DataTable, Text, Spinner, Tip } from "grommet"; +import { Filter } from "src/types"; +import { useHistory } from "react-router-dom"; +import { + Address, + formatNumber, + PaginationNavigator, + PaginationRecordsPerPage, + TipContent, + TokenValue, + TPaginationAction, +} from "src/components/ui"; +import { Erc20 } from "../../hooks/ERC20_Pool"; +import { CircleQuestion } from "grommet-icons"; + +interface TransactionTableProps { + data: any[]; + totalElements: number; + limit: number; + filter: Filter; + setFilter: (filter: Filter, action?: TPaginationAction) => void; + showIfEmpty?: boolean; + emptyText?: string; + isLoading?: boolean; + minWidth?: string; +} + +export function ERC20Table(props: TransactionTableProps) { + const history = useHistory(); + const { + data, + totalElements, + limit, + filter, + setFilter, + emptyText = "No data to display", + isLoading, + minWidth = "1310px", + } = props; + + if (isLoading) { + return ( + + + + ); + } + + if (!data.length) { + return ( + + {emptyText} + + ); + } + + return ( + <> + + + {Math.min(limit, data.length)} token + {data.length !== 1 ? "s" : ""} shown + + + + + + + + + + + + ); +} + +function getColumns(props: any) { + return [ + { + property: "name", + size: "small", + resizeable: false, + header: ( + + Name + + ), + render: (data: Erc20) => {data.name}, + }, + { + property: "symbol", + size: "xsmall", + resizeable: false, + header: ( + + Symbol + + ), + render: (data: Erc20) => {data.symbol}, + }, + { + property: "address", + primary: true, + header: ( + + Address + + ), + render: (data: Erc20) =>
, + }, + { + property: "totalSupply", + size: "small", + resizeable: false, + header: ( + + Circulating Supply + + ), + render: (data: Erc20) => { + console.log(data); + return ( + + + + } + plain + > + + + + + + ); + }, + }, + { + property: "totalSupply", + size: "small", + resizeable: false, + header: ( + + Total supply + + ), + render: (data: Erc20) => { + return ( + + + + } + plain + > + + + + + + ); + }, + }, + { + property: "holders", + size: "small", + resizeable: false, + header: ( + + Holders + + ), + render: (data: Erc20) => ( + + + {formatNumber(+data.holders)} + + + } + plain + > + + + + + + ), + }, + ]; +} diff --git a/src/pages/ERC20List/index.tsx b/src/pages/ERC20List/index.tsx new file mode 100644 index 0000000..06681ed --- /dev/null +++ b/src/pages/ERC20List/index.tsx @@ -0,0 +1,129 @@ +import { BasePage, TPaginationAction } from "src/components/ui"; + +import React, { useEffect, useState } from "react"; +import { Box, Heading, Spinner, Text, TextInput } from "grommet"; +import { Filter } from "src/types"; +import { Erc20, useERC20Pool } from "src/hooks/ERC20_Pool"; +import { useThemeMode } from "src/hooks/themeSwitcherHook"; +import { ERC20Table } from "./ERC20Table"; +import { Search } from "grommet-icons"; + +const initFilter: Filter = { + offset: 0, + limit: 10, + orderBy: "block_number", + orderDirection: "desc", + filters: [{ type: "gte", property: "block_number", value: 0 }], +}; + +export const ERC20List = () => { + const [data, setData] = useState([]); + const [filter, setFilter] = useState(initFilter); + const [search, setSearch] = useState(""); + const erc20 = useERC20Pool(); + const themeMode = useThemeMode(); + const erc20Tokens = Object.values(erc20); + + const searchedTokenLength = erc20Tokens.filter( + filterWithFields(["name", "symbol"], search) + ).length; + + useEffect(() => { + setData( + erc20Tokens + .filter(filterWithFields(["name", "symbol"], search)) + .sort(sortWithHolders) + //@ts-ignore + .slice(filter.offset, filter.offset + filter.limit) + ); + }, [erc20, filter, search]); + + useEffect(() => { + setFilter({ ...filter, offset: 0 }); + }, [search]); + + const onChangeFilter = (newFilter: Filter, action?: TPaginationAction) => { + //@ts-ignore + if (action === "prevPage" && filter.offset > 0) { + //@ts-ignore + newFilter.offset = Math.max(0, filter.offset - filter.limit); + } + + if ( + action === "nextPage" && + //@ts-ignore + (filter.offset + filter.limit < !!search + ? searchedTokenLength + : erc20Tokens.length) + ) { + newFilter.offset = Math.min( + //@ts-ignore + erc20Tokens.length, + //@ts-ignore + filter.offset + filter.limit + ); + } + + setFilter(newFilter); + }; + + const { limit = 10 } = filter; + + return ( + <> + + HRC20 Tokens + + + + setSearch(e.target.value)} + color="red" + icon={} + style={{ + backgroundColor: themeMode === "light" ? "white" : "transparent", + fontWeight: 500, + }} + placeholder="Search by Name / Symbol" + /> + + {!erc20Tokens.length && !search && ( + + + + )} + {!erc20Tokens.length && search && ( + + No tokens for this search + + )} + {!!erc20Tokens.length && ( + + )} + + + ); +}; + +function filterWithFields(fields: Array, search: string) { + return (erc20: Erc20) => { + return fields.some((field) => + (erc20 as any)[field] + .toString() + .toLowerCase() + .includes(search.toLowerCase()) + ); + }; +} + +function sortWithHolders(a: Erc20, b: Erc20) { + return +b.holders - +a.holders; +} diff --git a/src/pages/ERC721List/ERC721Table.tsx b/src/pages/ERC721List/ERC721Table.tsx new file mode 100644 index 0000000..da3b2c7 --- /dev/null +++ b/src/pages/ERC721List/ERC721Table.tsx @@ -0,0 +1,202 @@ +import React from "react"; + +import { Box, DataTable, Text, Spinner, Tip } from "grommet"; +import { Filter } from "src/types"; +import { useHistory } from "react-router-dom"; +import { + Address, + formatNumber, + PaginationNavigator, + PaginationRecordsPerPage, + TipContent, + TokenValue, + TPaginationAction, +} from "src/components/ui"; +import { Erc20 } from "../../hooks/ERC20_Pool"; +import { CircleQuestion } from "grommet-icons"; + +interface TransactionTableProps { + data: any[]; + totalElements: number; + limit: number; + filter: Filter; + setFilter: (filter: Filter, action?: TPaginationAction) => void; + showIfEmpty?: boolean; + emptyText?: string; + isLoading?: boolean; + minWidth?: string; +} + +export function ERC721Table(props: TransactionTableProps) { + const history = useHistory(); + const { + data, + totalElements, + limit, + filter, + setFilter, + emptyText = "No data to display", + isLoading, + minWidth = "1310px", + } = props; + + if (isLoading) { + return ( + + + + ); + } + + if (!data.length) { + return ( + + {emptyText} + + ); + } + + return ( + <> + + + {Math.min(limit, data.length)} token + {data.length !== 1 ? "s" : ""} shown + + + + + + + + + + + + ); +} + +function getColumns(props: any) { + return [ + { + property: "name", + size: "small", + resizeable: false, + header: ( + + Name + + ), + render: (data: Erc20) => {data.name}, + }, + { + property: "symbol", + size: "xsmall", + resizeable: false, + header: ( + + Symbol + + ), + render: (data: Erc20) => {data.symbol}, + }, + { + property: "address", + primary: true, + header: ( + + Address + + ), + render: (data: Erc20) => { +console.log(data) + return
+ }, + }, + // { + // property: "totalSupply", + // size: 'small', + // resizeable: false, + // header: ( + // + // Total supply + // + // ), + // render: (data: Erc20) => { + // return ; + // }, + // }, + { + property: "holders", + size: "small", + resizeable: false, + header: ( + + Holders + + ), + render: (data: Erc20) => ( + + + {formatNumber(+data.holders)} + + + } + plain + > + + + + + + ), + }, + ]; +} diff --git a/src/pages/ERC721List/index.tsx b/src/pages/ERC721List/index.tsx new file mode 100644 index 0000000..ba14b30 --- /dev/null +++ b/src/pages/ERC721List/index.tsx @@ -0,0 +1,125 @@ +import { BasePage, TPaginationAction } from "src/components/ui"; + +import React, { useEffect, useState } from "react"; +import { Box, Heading, Spinner, Text, TextInput } from "grommet"; +import { Filter } from "src/types"; +import { ERC721, useERC721Pool } from "src/hooks/ERC721_Pool"; +import { useThemeMode } from "src/hooks/themeSwitcherHook"; +import { ERC721Table } from "./ERC721Table"; +import { Search } from "grommet-icons"; + +const initFilter: Filter = { + offset: 0, + limit: 10, + orderBy: "block_number", + orderDirection: "desc", + filters: [{ type: "gte", property: "block_number", value: 0 }], +}; + +export const ERC721List = () => { + const [data, setData] = useState([]); + const [filter, setFilter] = useState(initFilter); + const [search, setSearch] = useState(""); + const erc721 = useERC721Pool(); + const themeMode = useThemeMode(); + const erc721Tokens = Object.values(erc721); + + const searchedTokenLength = erc721Tokens.filter( + filterWithFields(["name", "symbol"], search) + ).length; + + useEffect(() => { + setData( + erc721Tokens + .filter(filterWithFields(["name", "symbol"], search)) + .sort(sortWithHolders) + //@ts-ignore + .slice(filter.offset, filter.offset + filter.limit) + ); + }, [erc721, filter, search]); + + useEffect(() => { + setFilter({ ...filter, offset: 0 }); + }, [search]); + + const onChangeFilter = (newFilter: Filter, action?: TPaginationAction) => { + //@ts-ignore + if (action === "prevPage" && filter.offset > 0) { + //@ts-ignore + newFilter.offset = Math.max(0, filter.offset - filter.limit); + } + + if ( + action === "nextPage" && + //@ts-ignore + filter.offset + filter.limit < + (!!search ? searchedTokenLength : erc721Tokens.length) + ) { + newFilter.offset = Math.min( + //@ts-ignore + erc721Tokens.length, + //@ts-ignore + filter.offset + filter.limit + ); + } + + setFilter(newFilter); + }; + + const { limit = 10 } = filter; + + return ( + <> + + HRC721 Tokens + + + + setSearch(e.target.value)} + color="red" + icon={} + style={{ + backgroundColor: themeMode === "light" ? "white" : "transparent", + fontWeight: 500, + }} + placeholder="Search by Name / Symbol" + /> + + {!erc721Tokens.length && !search && ( + + + + )} + {!erc721Tokens.length && search && ( + + No tokens for this search + + )} + {!!erc721Tokens.length && ( + + )} + + + ); +}; + +function filterWithFields(fields: Array, search: string) { + return (erc721: ERC721) => { + return fields.some((field) => + erc721[field].toString().toLowerCase().includes(search.toLowerCase()) + ); + }; +} + +function sortWithHolders(a: ERC721, b: ERC721) { + return +b.holders - +a.holders; +} diff --git a/src/pages/InventoryDetailsPage/InventoryDetailsPage.tsx b/src/pages/InventoryDetailsPage/InventoryDetailsPage.tsx new file mode 100644 index 0000000..6bf76a7 --- /dev/null +++ b/src/pages/InventoryDetailsPage/InventoryDetailsPage.tsx @@ -0,0 +1,58 @@ +import { Heading } from "grommet"; +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { getTokenERC1155Assets, getTokenERC721Assets } from "src/api/client"; +import { IUserERC721Assets } from "src/api/client.interface"; +import { BasePage } from "src/components/ui"; +import { useERC1155Pool } from "src/hooks/ERC1155_Pool"; +import { useERC721Pool } from "src/hooks/ERC721_Pool"; + +export function InventoryDetailsPage() { + const erc721Map = useERC721Pool(); + const erc1155Map = useERC1155Pool(); + const [inventory, setInventory] = useState({} as any); + + // @ts-ignore + const { address, tokenID, type } = useParams(); + + const item = erc721Map[address] || erc1155Map[address]; + const name = item.name || ""; + + useEffect(() => { + const getInventory = async () => { + try { + if (type === "erc721" || type === "erc1155") { + let inventory = + type === "erc721" + ? await getTokenERC721Assets([address]) + : await getTokenERC1155Assets([address]); + + setInventory(inventory.filter((item) => item.tokenID === tokenID)[0]); + } else { + setInventory({} as any); + } + } catch (err) { + setInventory({} as any); + } + }; + getInventory(); + }, [address]); + + let meta = ""; + try { + meta = JSON.stringify(inventory.meta, null, 4); + } catch { + meta = ""; + } + + return ( + <> + + {name} {tokenID} {inventory.meta?.name || ""} + + +
{meta}
+
+ + ); +} diff --git a/src/pages/MainPage/LatestBlocksTable.tsx b/src/pages/MainPage/LatestBlocksTable.tsx new file mode 100644 index 0000000..221e465 --- /dev/null +++ b/src/pages/MainPage/LatestBlocksTable.tsx @@ -0,0 +1,116 @@ +import React from "react"; +import { Box, DataTable, Spinner, Text } from "grommet"; +import { Block } from "src/types"; +import { useHistory } from "react-router-dom"; +import { formatNumber, RelativeTimer } from "src/components/ui"; + +function getColumns(props: any) { + const { history } = props; + return [ + { + property: "shard", + header: ( + + Shard + + ), + render: (data: Block) => ( + history.push(`/blocks/shard/${data.shardNumber}`)} + style={{ cursor: "pointer" }} + color={"brand"} + > + {data.shardNumber} + + ), + }, + { + property: "number", + header: ( + + Height + + ), + render: (data: Block) => ( + { + history.push(`/block/${data.hash}`); + }} + color="brand" + > + {formatNumber(+data.number)} + + ), + }, + { + property: "transactions", + header: ( + + Transactions + + ), + render: (data: Block) => ( + + {data.transactions.length + data.stakingTransactions.length} + + ), + }, + { + property: "timestamp", + header: ( + + Timestamp + + ), + render: (data: Block) => ( + + {/**/} + {/* {dayjs(data.timestamp).format("YYYY-MM-DD, HH:mm:ss")},*/} + {/**/} + + + ), + }, + ]; +} + +export const LatestBlocksTable = (params: { blocks: Block[] }) => { + const history = useHistory(); + + if (!params.blocks.length) { + return ( + + + + ); + } + + return ( + + + + ); +}; diff --git a/src/pages/MainPage/LatestTransactionsTable.tsx b/src/pages/MainPage/LatestTransactionsTable.tsx new file mode 100644 index 0000000..8ea1fdc --- /dev/null +++ b/src/pages/MainPage/LatestTransactionsTable.tsx @@ -0,0 +1,170 @@ +import React, { useEffect, useState } from "react"; + +import { Box, DataTable, Spinner, Text } from "grommet"; +import { RPCTransactionHarmony } from "src/types"; +import { useHistory } from "react-router-dom"; +import { RelativeTimer, Address } from "src/components/ui"; +import { getTransactions } from "src/api/client"; +import { FormNextLink } from "grommet-icons"; + +function getColumns(props: any) { + const { history } = props; + return [ + { + property: "shard", + header: ( + + Shard + + ), + render: (data: RPCTransactionHarmony) => ( + + {data.shardID} + + {data.toShardID} + + ), + }, + { + property: "hash", + header: ( + + Hash + + ), + render: (data: RPCTransactionHarmony) => ( + { + history.push(`/tx/${data.hash}`); + }} + color="brand" + > +
+ + ), + }, + { + property: "from", + header: ( + + From + + ), + render: (data: RPCTransactionHarmony) => ( +
+ ), + }, + { + property: "to", + header: ( + + To + + ), + render: (data: RPCTransactionHarmony) => ( +
+ ), + }, + { + property: "age", + header: ( + + Timestamp + + ), + render: (data: RPCTransactionHarmony) => ( + + ), + }, + ]; +} + +const filter = { + offset: 0, + limit: 10, + orderBy: "block_number", + orderDirection: "desc", + value: 0, + filters: [], +}; + +export function LatestTransactionsTable() { + const history = useHistory(); + const [transactions, setTransactions] = useState([]); + const availableShards = (process.env.REACT_APP_AVAILABLE_SHARDS as string) + .split(",") + .map((t) => +t); + + useEffect(() => { + let tId = 0 as any; + const exec = async () => { + try { + let trxs = await Promise.all( + availableShards.map((shardNumber) => + getTransactions([shardNumber, filter]) + ) + ); + + const trxsList = trxs.reduce((prev, cur) => { + prev = [...prev, ...cur]; + return prev; + }, []); + + setTransactions( + trxsList + .sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)) + .slice(0, 10) as RPCTransactionHarmony[] + ); + } catch (err) { + console.log(err); + } + }; + + exec(); + tId = window.setInterval(exec, 3000); + + return () => { + clearTimeout(tId); + }; + }, []); + + if (!transactions.length) { + return ( + + + + ); + } + + return ( + + + + ); +} diff --git a/src/pages/MainPage/helpers.ts b/src/pages/MainPage/helpers.ts new file mode 100644 index 0000000..4eca882 --- /dev/null +++ b/src/pages/MainPage/helpers.ts @@ -0,0 +1,24 @@ +import { Block } from "src/types"; + +const getLatency = (blocks: Block[]) => { + const blocksTimestamp = blocks + .map((b) => new Date(b.timestamp).getTime()) + .sort((a, b) => (a < b ? -1 : 1)); + + const diffs = []; + + for (let i = blocksTimestamp.length - 1; i > 0; i--) { + diffs.push(blocksTimestamp[i] - blocksTimestamp[i - 1]); + } + + return diffs.reduce((acc, t) => acc + t, 0) / diffs.length / 1000; +}; + +export const calculateSecondPerBlocks = ( + all_blocks: Array +): number => { + return ( + all_blocks.map(getLatency).reduce((acc, t) => acc + t, 0) / + all_blocks.length + ); +}; diff --git a/src/pages/MainPage/index.tsx b/src/pages/MainPage/index.tsx new file mode 100644 index 0000000..2e250cc --- /dev/null +++ b/src/pages/MainPage/index.tsx @@ -0,0 +1,118 @@ +import React, { useEffect, useState } from "react"; +import { Box, Text } from "grommet"; +import { Button } from "src/components/ui"; +import { useHistory } from "react-router-dom"; +import { useMediaQuery } from "react-responsive"; +import { breakpoints } from "src/Responive/breakpoints"; +import { BaseContainer, BasePage } from "src/components/ui"; +import { Metrics } from "src/components/metrics"; +import { LatestBlocksTable } from "./LatestBlocksTable"; +import { LatestTransactionsTable } from "./LatestTransactionsTable"; +import { Block } from "src/types"; +import { getBlocks } from "src/api/client"; +import { calculateSecondPerBlocks } from "./helpers"; + +const filter = { + offset: 0, + limit: 10, + orderBy: "number", + orderDirection: "desc", + value: 0, + filters: [], +}; + +export function MainPage() { + const history = useHistory(); + const isLessDesktop = useMediaQuery({ maxDeviceWidth: breakpoints.desktop }); + + const [blocks, setBlocks] = useState([]); + const [blockLatency, setBlockLatency] = useState(2.01); + const availableShards = (process.env + .REACT_APP_AVAILABLE_SHARDS as string).split(","); + + useEffect(() => { + let tId = 0 as any; + const exec = async () => { + try { + let blocks = await Promise.all( + availableShards.map((shardNumber) => + getBlocks([+shardNumber, filter]) + ) + ); + + const blocksList = blocks.reduce((prev, cur, index) => { + prev = [ + ...prev, + ...cur.map((item) => ({ + ...item, + shardNumber: +availableShards[index], + })), + ]; + return prev; + }, []); + + setBlocks( + blocksList + .sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)) + .slice(0, 10) + ); + + setBlockLatency(calculateSecondPerBlocks(blocks)); + } catch (err) { + console.log(err); + } + }; + + exec(); + tId = window.setInterval(exec, 3000); + + return () => { + clearTimeout(tId); + }; + }, []); + + return ( + + + + + + + Latest Blocks + + + + + + + + + + Latest Transactions + + + + + + + + ); +} diff --git a/src/pages/StackingTransactionPage/index.tsx b/src/pages/StackingTransactionPage/index.tsx new file mode 100644 index 0000000..396cc9b --- /dev/null +++ b/src/pages/StackingTransactionPage/index.tsx @@ -0,0 +1,63 @@ +import { RPCStakingTransactionHarmony } from "src/types"; +import { BasePage } from "src/components/ui"; + +import { useParams } from "react-router-dom"; +import React, { useEffect, useState } from "react"; +import { Box, Text } from "grommet"; +import { getStakingTransactionByField } from "src/api/client"; +import { TransactionDetails } from "src/components/transaction/TransactionDetails"; +import { StakingTransactionType } from "src/types"; +import { TransactionSubType } from "src/components/transaction/helpers"; + +export const StakingTransactionPage = () => { + // @ts-ignore + const { id } = useParams(); + const [tx, setTx] = useState(null); + + useEffect(() => { + const exec = async () => { + let tx; + if (id.length === 66) { + tx = await getStakingTransactionByField([0, "hash", id]); + } + setTx(tx as RPCStakingTransactionHarmony); + }; + exec(); + }, [id]); + + if (!tx) { + return null; + } + + return ( + + + + Staking Transaction + + + + + + Staking Data + + + + ); +}; + +const subTypeMap: Record = { + Delegate: "__delegated", + Undelegate: "__undelegated", + CollectRewards: "", + CreateValidator: "", + EditValidator: "", +}; diff --git a/src/pages/TransactionPage/index.tsx b/src/pages/TransactionPage/index.tsx new file mode 100644 index 0000000..31f2ddf --- /dev/null +++ b/src/pages/TransactionPage/index.tsx @@ -0,0 +1,217 @@ +import { TransactionDetails } from "src/components/transaction/TransactionDetails"; +import { InternalTransactionList } from "src/components/transaction/InternalTransactionList"; +import { TransactionLogs } from "src/components/transaction/TransactionLogs"; +import { InternalTransaction, RPCStakingTransactionHarmony } from "src/types"; +import { BaseContainer, BasePage } from "src/components/ui"; + +import { useHistory, useParams } from "react-router-dom"; +import React, { useEffect, useState } from "react"; +import { Tabs, Tab, Text, Box, Spinner, Heading } from "grommet"; +import { + getInternalTransactionsByField, + getTransactionByField, + getTransactionLogsByField, + getByteCodeSignatureByHash, +} from "src/api/client"; +import { AllBlocksTable } from "../AllBlocksPage/AllBlocksTable"; +import { revertErrorMessage } from "src/web3/parseByteCode"; + +const extractError = (err: any) => { + const errorMessages = err!.split(":"); + if (errorMessages[1]) { + const errorMessage = revertErrorMessage(errorMessages[1]); + return errorMessage || err; + } + + const errorMessage = revertErrorMessage(err); + return errorMessage || err; +}; + +export const TransactionPage = () => { + const history = useHistory(); + const tabParamName = "activeTab="; + let activeTab = 0; + try { + activeTab = +history.location.search.slice( + history.location.search.indexOf("activeTab=") + tabParamName.length + ); + } catch { + activeTab = 0; + } + + // hash or number + // @ts-ignore + const { id } = useParams(); + const [tx, setTx] = useState( + {} as RPCStakingTransactionHarmony + ); + const [trxs, setTrxs] = useState([]); + const [logs, setLogs] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [txrsLoading, setTxrsLoading] = useState(true); + const [activeIndex, setActiveIndex] = useState(+activeTab); + + const availableShards = (process.env.REACT_APP_AVAILABLE_SHARDS as string) + .split(",") + .map((t) => +t); + + useEffect(() => { + const getTx = async () => { + let trx; + if (id.length === 66) { + trx = await getTransactionByField([0, "hash", id]); + } + + if (!trx && availableShards.find((i) => i === 1)) { + trx = await getTransactionByField([1, "hash", id]); + } + + if (!trx && availableShards.find((i) => i === 2)) { + trx = await getTransactionByField([2, "hash", id]); + } + + if (!trx && availableShards.find((i) => i === 3)) { + trx = await getTransactionByField([3, "hash", id]); + } + + setTx(trx as RPCStakingTransactionHarmony); + }; + + getTx(); + }, [id]); + + useEffect(() => { + const getInternalTxs = async () => { + if (tx.hash && tx.shardID === 0) { + try { + //@ts-ignore + const txs = await getInternalTransactionsByField([ + 0, + "transaction_hash", + tx.hash, + ]); + const methodSignatures = await Promise.all( + txs.map((tx) => { + return tx.input && tx.input.length > 10 + ? getByteCodeSignatureByHash([tx.input.slice(0, 10)]) + : Promise.resolve([]); + }) + ); + + const txsWithSignatures = txs.map((l, i) => ({ + ...l, + signatures: methodSignatures[i], + })); + + setTrxs(txsWithSignatures as InternalTransaction[]); + setTxrsLoading(false); + } catch (err) { + console.log(err); + } + } else { + setTrxs([]); + } + }; + + getInternalTxs(); + }, [tx.hash]); + + useEffect(() => { + const getLogs = async () => { + if (tx.hash && tx.shardID === 0) { + try { + //@ts-ignore + const logs: any[] = await getTransactionLogsByField([ + 0, + "transaction_hash", + tx.hash, + ]); + + const logsSignatures = await Promise.all( + logs.map((l) => getByteCodeSignatureByHash([l.topics[0]])) + ); + + const logsWithSignatures = logs.map((l, i) => ({ + ...l, + signatures: logsSignatures[i], + })); + + setLogs(logsWithSignatures as any); + setIsLoading(false); + } catch (err) { + console.log(err); + } + } else { + setLogs([]); + } + }; + + getLogs(); + }, [tx]); + + if (isLoading) { + return ( + + + + ); + } + + return ( + + + Transaction + + + { + history.replace( + `${history.location.pathname}?activeTab=${newActive}` + ); + console.log(history); + setActiveIndex(newActive); + }} + > + Transaction Details}> + t.error) + .filter((_) => _) + .map(extractError) + .join(",") + : "" + } + /> + + {trxs.length ? ( + Internal Transactions ({trxs.length}) + } + > + + + ) : null} + {logs.length ? ( + Logs ({logs.length})}> + + + ) : null} + + + + ); +}; diff --git a/src/pages/VerifyContract/VerifyContract.tsx b/src/pages/VerifyContract/VerifyContract.tsx new file mode 100644 index 0000000..4f1920a --- /dev/null +++ b/src/pages/VerifyContract/VerifyContract.tsx @@ -0,0 +1,279 @@ +import { + Box, + Heading, + Select, + Spinner, + Text, + TextArea, + TextInput, +} from "grommet"; +import React from "react"; +import { BaseContainer, BasePage, Button } from "src/components/ui"; +import styled from "styled-components"; +import { IVerifyContractData, verifyContractCode } from "src/api/explorerV1"; +import { CircleAlert, StatusGood, SubtractCircle } from "grommet-icons"; +import { toaster } from "src/App"; + +const Field = styled(Box)``; + +const Wrapper = styled(Box)` + & * input, + & * textarea { + font-weight: 400 !important; + } +`; + +export function uniqid(prefix = "", random = false) { + const sec = Date.now() * 1000 + Math.random() * 1000; + const id = sec.toString(16).replace(/\./g, "").padEnd(14, "0"); + return `${prefix}${id}${ + random ? `.${Math.trunc(Math.random() * 100000000)}` : "" + }`; +} + +export class VerifyContract extends React.Component< + { + isLessTablet: boolean; + }, + IVerifyContractData +> { + public state: IVerifyContractData = { + chainType: "mainnet", + contractAddress: "", + compiler: "", + optimizer: "no", + optimizerTimes: "", + sourceCode: "", + libraries: [], + constructorArguments: "", + contractName: "", + isLoading: false, + statusText: "", + }; + + onClickSubmitBtn = async () => { + this.setState({ ...this.state, isLoading: true, statusText: "Pending..." }); + const { isLoading, statusText, ...state } = this.state; + + try { + const res = await verifyContractCode({ + ...state, + libraries: this.state.libraries.map((i) => i.value), + }); + + if (res === true) { + this.setState({ ...this.state, statusText: "Success" }); + } else { + this.setState({ ...this.state, statusText: "Error" }); + } + } catch { + this.setState({ ...this.state, statusText: "Error" }); + } finally { + this.setState({ ...this.state, isLoading: false }); + } + }; + + render() { + const { isLessTablet } = this.props; + const { isLoading } = this.state; + + return ( + <> + + Verify Contract + + + + + + Contract Address + ) => { + this.setState({ + ...this.state, + contractAddress: evt.currentTarget.value, + }); + }} + disabled={isLoading} + /> + + + + Contract Name + ) => { + this.setState({ + ...this.state, + contractName: evt.currentTarget.value, + }); + }} + disabled={isLoading} + /> + + + + + + Chain Type + + this.setState({ ...this.state, optimizer: option }) + } + disabled={isLoading} + /> + ) => { + this.setState({ + ...this.state, + optimizerTimes: evt.currentTarget.value, + }); + }} + disabled={isLoading} + /> + + + + + + Enter the Solidity Contract Code below +