pull/34/head
jenya 3 years ago
commit 5cae66ee5a
  1. 18
      .dockerignore
  2. 9
      .env.example
  3. 36
      .gitignore
  4. 13
      Dockerfile
  5. 46
      README.md
  6. 66
      package.json
  7. 33
      public/404.html
  8. 1
      public/env.js
  9. BIN
      public/favicon.ico
  10. 56
      public/index.html
  11. BIN
      public/logo192.png
  12. BIN
      public/logo512.png
  13. 25
      public/manifest.json
  14. 3
      public/robots.txt
  15. 81
      src/App.tsx
  16. 8
      src/Responive/breakpoints.ts
  17. 84
      src/Routes.tsx
  18. 22
      src/api/ApiCache.ts
  19. 57
      src/api/client.interface.ts
  20. 166
      src/api/client.ts
  21. 73
      src/api/clientCache.ts
  22. 1
      src/api/explorer/index.ts
  23. 25
      src/api/explorer/ws/index.ts
  24. 91
      src/api/explorerV1.ts
  25. 83
      src/api/rpc.ts
  26. 5
      src/assets/Logo.svg
  27. 37
      src/components/BinancePairHistoricalPrice_Pool.tsx
  28. 31
      src/components/ERC1155_Pool.tsx
  29. 26
      src/components/ERC20Value.tsx
  30. 31
      src/components/ERC20_Pool.tsx
  31. 32
      src/components/ERC721_Pool.tsx
  32. 50
      src/components/ONE_USDT_Rate.tsx
  33. 53
      src/components/appFooter/index.tsx
  34. 117
      src/components/appHeader/ConfigureButton.tsx
  35. 68
      src/components/appHeader/InfoButton.tsx
  36. 72
      src/components/appHeader/index.tsx
  37. 121
      src/components/block/BlockDetails.tsx
  38. 94
      src/components/block/BlockList.tsx
  39. 241
      src/components/block/helpers.tsx
  40. 227
      src/components/dropdown/Dropdown.tsx
  41. 297
      src/components/metrics/index.tsx
  42. 34
      src/components/pagination/Pagination.tsx
  43. 85
      src/components/tables/TableComponents.tsx
  44. 286
      src/components/tables/TransactionsTable.tsx
  45. 142
      src/components/transaction/InternalTransactionList.tsx
  46. 213
      src/components/transaction/TransactionDetails.tsx
  47. 1
      src/components/transaction/TransactionList.tsx
  48. 107
      src/components/transaction/TransactionLogs.tsx
  49. 279
      src/components/transaction/helpers.tsx
  50. 125
      src/components/ui/Address.tsx
  51. 1
      src/components/ui/Amount.ts
  52. 14
      src/components/ui/AnchorLink.tsx
  53. 40
      src/components/ui/BaseContainer.tsx
  54. 10
      src/components/ui/BlockHash.tsx
  55. 19
      src/components/ui/BlockNumber.tsx
  56. 33
      src/components/ui/Button/index.tsx
  57. 37
      src/components/ui/CopyBtn.tsx
  58. 80
      src/components/ui/ERC1155Icon.tsx
  59. 37
      src/components/ui/ExpandString.tsx
  60. 29
      src/components/ui/FiatPrice.tsx
  61. 1
      src/components/ui/HexData.ts
  62. 81
      src/components/ui/ONEValue.tsx
  63. 107
      src/components/ui/OneValueDropdown.tsx
  64. 147
      src/components/ui/Pagination/index.tsx
  65. 120
      src/components/ui/PaginationBlock/index.tsx
  66. 86
      src/components/ui/RelativeTimer.tsx
  67. 301
      src/components/ui/Search.tsx
  68. 34
      src/components/ui/ShardDropdown.tsx
  69. 23
      src/components/ui/StakingTransactionType.tsx
  70. 21
      src/components/ui/Timestamp.tsx
  71. 56
      src/components/ui/TokenValue.tsx
  72. 102
      src/components/ui/TokenValueBalanced.tsx
  73. 13
      src/components/ui/Tooltip.tsx
  74. 9
      src/components/ui/TransactionHash.tsx
  75. 23
      src/components/ui/TransactionType.tsx
  76. 37
      src/components/ui/TxStatusComponent.tsx
  77. 106
      src/components/ui/icons.tsx
  78. 21
      src/components/ui/index.ts
  79. 31
      src/components/ui/toaster/Toaster.ts
  80. 77
      src/components/ui/toaster/ToasterComponent.tsx
  81. 2
      src/components/ui/toaster/index.ts
  82. 68
      src/components/ui/utils.tsx
  83. 7
      src/config/BinanceAddressMap.ts
  84. 1
      src/config/index.ts
  85. 32
      src/hooks/BinancePairHistoricalPrice.ts
  86. 37
      src/hooks/ERC1155_Pool.ts
  87. 42
      src/hooks/ERC20_Pool.ts
  88. 36
      src/hooks/ERC721_Pool.ts
  89. 26
      src/hooks/ONE-ETH-SwitcherHook.ts
  90. 75
      src/hooks/polling.tsx
  91. 26
      src/hooks/themeSwitcherHook.ts
  92. 23
      src/hooks/useONEExchangeRate.tsx
  93. 40
      src/index.css
  94. 17
      src/index.tsx
  95. 275
      src/pages/AddressPage/AddressDetails.tsx
  96. 215
      src/pages/AddressPage/ContractDetails/AbiMethodView.tsx
  97. 55
      src/pages/AddressPage/ContractDetails/ConnectWallets.tsx
  98. 14
      src/pages/AddressPage/ContractDetails/helpers.ts
  99. 240
      src/pages/AddressPage/ContractDetails/index.tsx
  100. 303
      src/pages/AddressPage/TokenInfo.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

@ -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

@ -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

36
.gitignore vendored

@ -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

@ -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"]

@ -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/).

@ -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"
}
}

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page Not Found</title>
<style media="screen">
body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
#message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px 16px; border-radius: 3px; }
#message h3 { color: #888; font-weight: normal; font-size: 16px; margin: 16px 0 12px; }
#message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
#message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
#message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
#message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
#message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
#load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
@media (max-width: 600px) {
body, #message { margin-top: 0; background: white; box-shadow: none; }
body { border-top: 16px solid #ffa100; }
}
</style>
</head>
<body>
<div id="message">
<h2>404</h2>
<h1>Page Not Found</h1>
<p>The specified file was not found on this website. Please check the URL for mistakes and try again.</p>
<h3>Why am I seeing this?</h3>
<p>This page was generated by the Firebase Command-Line Interface. To modify it, edit the <code>404.html</code> file in your project's configured <code>public</code> directory.</p>
</div>
</body>
</html>

@ -0,0 +1 @@
window.env = {};

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link
href="https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;700&amp;display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@300;400;700&amp;display=swap"
rel="stylesheet"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Harmony block explorer"
/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Harmony Blockchain Explorer</title>
<style type="text/css">
body {
margin: 0;
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

@ -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"
}

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

@ -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 (
<Router>
<AppWithHistory />
</Router>
);
}
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 (
<Grommet
theme={themeMode === "light" ? theme : darkTheme}
themeMode={themeMode}
full
id="scrollBody"
>
<ToasterComponent toaster={toaster} />
<ERC20_Pool />
<ERC721_Pool />
<ERC1155_Pool />
<Box
background="backgroundBack"
style={{ margin: "auto", minHeight: "100%" }}
>
<AppHeader style={{ flex: "0 0 auto" }} />
<Box align="center" style={{ flex: "1 1 100%" }}>
<BaseContainer>
<SearchInput />
<Routes />
</BaseContainer>
</Box>
<AppFooter style={{ flex: "0 0 auto" }} />
<ONE_USDT_Rate />
</Box>
</Grommet>
);
}
export default App;

@ -0,0 +1,8 @@
export const breakpoints = {
mobile: '375px',
mobileL: '425px',
tablet: '768px',
tabletM: '868px',
laptop: '1024px',
desktop: '1366px',
};

@ -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 (
<>
<Switch>
<Route exact path="/">
<MainPage />
</Route>
<Route exact path="/blocks">
{/* <AllBlocksPage /> */}
<Redirect to="/blocks/shard/0" />
</Route>
<Route exact path="/blocks/shard/:shardNumber">
<AllBlocksPage />
</Route>
<Route path="/block/:id">
<BlockPage />
</Route>
<Route exact path="/transactions">
{/* <AllTransactionsPage /> */}
<Redirect to="/transactions/shard/0" />
</Route>
<Route exact path="/transactions/shard/:shardNumber">
<AllTransactionsPage />
</Route>
<Route path="/tx/:id">
<TransactionPage />
</Route>
<Route path="/staking-tx/:id">
<StakingTransactionPage />
</Route>
<Route path="/address/:id">
<AddressPage />
</Route>
<Route path="/inventory/:type/:address/:tokenID">
<InventoryDetailsPage />
</Route>
<Route path="/hrc20">
<ERC20List />
</Route>
<Route path="/hrc721">
<ERC721List />
</Route>
<Route path="/hrc1155">
<ERC1155List />
</Route>
<Route path="/verifycontract">
<VerifyContract isLessTablet={isLessTablet} />
</Route>
</Switch>
</>
);
}

@ -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<T = any>(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 }));
}
}

@ -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;
}

@ -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<Block>;
}
export function getBlockByHash(params: any[]) {
return transport("getBlockByHash", params) as Promise<Block>;
}
export function getBlocks(params: any[]) {
return transport("getBlocks", params) as Promise<Block[]>;
}
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<RPCStakingTransactionHarmony>;
}
export function getStakingTransactionByField(params: [number, "hash", string]) {
return transport(
"getStakingTransactionsByField",
params
) as Promise<RPCStakingTransactionHarmony>;
}
export function getInternalTransactionsByField(params: any[]) {
return transport("getInternalTransactionsByField", params) as Promise<
InternalTransaction[]
>;
}
export function getTransactionLogsByField(params: any[]) {
return transport("getLogsByField", params) as Promise<any>;
}
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<any>);
}
export function getRelatedTransactions(params: any[]) {
return transport("getRelatedTransactions", params) as Promise<
RelatedTransaction[]
>;
}
export function getTransactionCountLast14Days() {
return transport("getTransactionCountLast14Days", []) as Promise<any[]>;
}
export function getContractsByField(params: any[]) {
return transport("getContractsByField", params) as Promise<any[]>;
}
export function getAllERC20() {
return transport("getAllERC20", []) as Promise<any[]>;
}
export function getAllERC721() {
return transport("getAllERC721", []) as Promise<any[]>;
}
export function getAllERC1155() {
return transport("getAllERC1155", []) as Promise<any[]>;
}
export function getUserERC20Balances(params: any[]) {
return transport("getUserERC20Balances", params) as Promise<any[]>;
}
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<IPairPrice>("getBinancePairPrice", params).then((res) => {
pairCache[params[0]] = res;
return res;
});
}
export function getBinancePairHistoricalPrice(params: [string]) {
return transport("getBinancePairHistoricalPrice", params) as Promise<any[]>;
}

@ -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<T = any>(
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;
}
}
}

@ -0,0 +1 @@
export {transport} from './ws'

@ -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 = <T = any>(method: string, params: any[]) => {
return new Promise<T>((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);
}
});
});
};

@ -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<ISourceCode> => {
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[];
}

@ -0,0 +1,83 @@
export type TRPCResponse<T> = { id: number; jsonrpc: "2.0"; result: T };
export const rpcAdapter = <T = any>(...args: Parameters<typeof fetch>) => {
/**
* wrapper for fetch. for some middleware in future requests
*/
return (fetch
.apply(window, args)
.then((res) => res.json()) as unknown) as Promise<T>;
};
export const getBalance = (params: [string, "latest"]) => {
return rpcAdapter<TRPCResponse<string>>("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<TRPCResponse<string>>(
`${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<TRPCResponse<string>>(
`${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<TRPCResponse<string>>(
`${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<TRPCResponse<string>>(
`${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));
});
};

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" viewBox="0 0 34 34" fill="none">
<path d="M26.232 0C22.292 0 19.0778 3.2142 18.9741 7.15419V15.1378C18.2484 15.1378 17.5226 15.2415 16.6931 15.2415C15.8636 15.2415 15.1378 15.2415 14.4121 15.3452V7.15419C14.4121 3.2142 11.0942 0 7.0505 0.103684C3.2142 0.103684 0.103684 3.31788 0 7.15419V26.232C0.103684 30.172 3.31788 33.3862 7.36155 33.2825C11.3015 33.1788 14.4121 30.0683 14.4121 26.232V18.2484C15.1378 18.2484 15.8636 18.1447 16.6931 18.1447C17.5226 18.1447 18.2484 18.1447 18.9741 18.041V26.1283C19.0778 30.0683 22.292 33.2825 26.3357 33.1788C30.2757 33.0751 33.3862 29.9646 33.3862 26.1283V7.15419C33.3862 3.2142 30.172 0 26.232 0ZM7.25787 2.90315C9.6426 2.90315 11.5089 4.76946 11.5089 7.15419V15.5526C9.43523 15.8636 7.36155 16.4857 5.39156 17.3152C4.56209 17.7299 3.73262 18.1447 3.00683 18.7668V7.15419C3.00683 4.87314 4.87314 2.90315 7.25787 2.90315ZM11.5089 26.232C11.5089 28.6167 9.6426 30.483 7.25787 30.483C4.87314 30.483 3.00683 28.6167 3.00683 26.232V24.3657C3.00683 22.7068 4.35472 21.0478 6.63577 20.1147C8.19102 19.3889 9.84997 18.9741 11.6126 18.6631L11.5089 26.232ZM26.232 30.483C23.8473 30.483 21.981 28.6167 21.981 26.232V17.8336C24.0547 17.5226 26.1283 16.9005 28.0983 16.071C28.9278 15.6563 29.7573 15.2415 30.4831 14.6194V26.232C30.4831 28.6167 28.5131 30.483 26.232 30.483ZM26.8541 13.3752C25.2989 14.101 23.6399 14.5157 21.8773 14.8268V7.15419C21.8773 4.76946 23.7436 2.90315 26.1283 2.90315C28.5131 2.90315 30.3794 4.76946 30.3794 7.15419V9.02049C30.4831 10.7831 29.1352 12.3384 26.8541 13.3752Z" fill="#fff"/>
<defs>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -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<string, Erc20Price>;
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;
}

@ -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<string, ERC1155>;
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;
}

@ -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 : ""}
</>
);
}

@ -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<string, Erc20>;
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;
}

@ -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<string, ERC721>;
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;
}

@ -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<string, number>;
fetch("https://api.binance.com/api/v3/klines?symbol=ONEUSDT&interval=1d")
.then((_res) => _res.json())
.then((res) => {
res.forEach((t: Array<string | number>) => {
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<string, number>;
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;
}

@ -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 (
<Box background="background" justify="center" align="center" pad="medium" margin={{ top: 'medium' }} {...props}>
<Box gap="xsmall">
<Box direction="row" width="320px" justify="center" align="center" gap="medium">
<IconAhchor href="https://harmony.one/team" target="_blank" rel="noreferrer">
<Group size="24px" color="minorText" style={{ cursor: 'pointer'}} />
</IconAhchor>
<IconAhchor href="https://harmony.one/discord" target="_blank" rel="noreferrer">
<DiscordIcon size="23px" color="minorText" />
</IconAhchor>
<IconAhchor href="https://medium.com/harmony-one" target="_blank" rel="noreferrer">
<Medium size="23px" color="minorText" style={{ cursor: 'pointer'}} />
</IconAhchor>
<IconAhchor href="https://t.me/harmony_one" target="_blank" rel="noreferrer">
<TelegramIcon size="22px" color="minorText" />
</IconAhchor>
<IconAhchor href="https://twitter.com/harmonyprotocol" target="_blank" rel="noreferrer">
<Twitter size="24px" color="minorText" style={{ cursor: 'pointer'}} />
</IconAhchor>
</Box>
{/*<Box direction="row" justify="center" align="center" gap="xsmall">*/}
{/* <Anchor color="minorText" size="small" weight="normal" href="/">Terms of Use</Anchor>*/}
{/* <Text color="minorText" size="medium">|</Text>*/}
{/* <Anchor color="minorText" size="small" weight="normal" href="/">Privacy Policy</Anchor>*/}
{/*</Box>*/}
<Box direction="row" justify="center" align="center" gap="xsmall">
<Text color="minorText" size="xsmall" margin={{ top: '3px' }}>©</Text>
<Text color="minorText" size="xsmall">Harmony {new Date().getFullYear()}</Text>
<Text color="minorText" size="small" margin={{ bottom: '6px' }}>.</Text>
<Text color="minorText" size="xsmall">hello@harmony.one</Text>
</Box>
</Box>
</Box>
)
}

@ -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 (
<DropButton
label={<Menu size="medium" color={'#fff'} />}
dropAlign={{ top: "bottom", right: "right" }}
style={{
border: "none",
boxShadow: "none",
paddingRight: "6px",
paddingLeft: 0,
}}
dropContent={
<Box
pad="medium"
background="background"
border={{ size: "xsmall", color: "border" }}
style={{ borderRadius: "0px" }}
>
<Text size="small" weight="bold" margin={{ bottom: "xsmall" }}>
Theme
</Text>
<ToggleButton
value={theme}
options={[
{ text: "Light", value: "light" },
{ text: "Dark", value: "dark" },
]}
onChange={setThemeMode}
/>
<Text
size="small"
weight="bold"
margin={{ bottom: "xsmall", top: "small" }}
>
Address style
</Text>
<ToggleButton
value={currency}
options={[
{ text: "Harmony", value: "ONE" },
{ text: "ETH", value: "ETH" },
]}
onChange={setCurrency}
/>
</Box>
}
/>
);
}
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 (
<Box
direction="row"
background="transparent"
border={{ size: "xsmall", color: "border" }}
style={{ overflow: "hidden", borderRadius: "8px" }}
>
{options.map((i) => (
<SwitchButton
selected={i.value === value}
onClick={() => onChange(i.value)}
key={i.value}
>
{i.text}
</SwitchButton>
))}
</Box>
);
};
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")};
`;

@ -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 (
<DropButton
label={
<Box direction={"row"} align="center">
<Text size="small" color="white" weight="bold">
Tokens
</Text>
<CaretDownFill color="white" />
</Box>
}
dropAlign={{ top: "bottom", right: "right" }}
dropContent={
<Box
pad="medium"
background="background"
border={{ size: "xsmall", color: "border" }}
style={{ borderRadius: "0px" }}
gap="small"
>
<Anchor
style={{ textDecoration: "underline" }}
href={"/hrc20"}
onClick={(e) => {
e.preventDefault();
history.push("/hrc20");
}}
>
HRC20 tokens
</Anchor>
<Anchor
style={{ textDecoration: "underline" }}
href={"/hrc721"}
onClick={(e) => {
e.preventDefault();
history.push("/hrc721");
}}
>
HRC721 tokens
</Anchor>
<Anchor
style={{ textDecoration: "underline" }}
href={"/hrc1155"}
onClick={(e) => {
e.preventDefault();
history.push("/hrc1155");
}}
>
HRC1155 tokens
</Anchor>
</Box>
}
style={{
border: "none",
boxShadow: "none",
paddingRight: "6px",
paddingBottom: "8px",
}}
/>
);
}

@ -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 (
<Box
tag="header"
direction="row"
justify="center"
background={isDark ? "background" : "brand"}
pad={{ vertical: "small" }}
elevation={isDark ? "none" : "medium"}
style={{ zIndex: "1" }}
{...props}
/>
);
};
const ProjectName = styled(Box)`
margin-left: 3px;
`;
export function AppHeader(props: { style: CSSProperties }) {
const history = useHistory();
return (
<HeaderLine
{...props}
style={{ boxShadow: "0px 4px 8px rgb(0 0 0 / 12%)" }}
>
<BaseContainer direction="row" align="center" justify="between" flex>
<Heading
level="5"
margin="none"
style={{
cursor: "pointer",
color: "#fff",
}}
onClick={() => history.push("/")}
>
<Box direction={"row"} align={"center"}>
<img src={require("../../assets/Logo.svg").default} />
<ProjectName direction={"row"} align={'center'}>
Harmony Block Explorer{" "}
<Box
background={"mintGreen"}
style={{ borderRadius: "8px", height: "20px", marginLeft: '5px', padding: '1px 7px' }}
>
<Text size={"xsmall"}>beta</Text>
</Box>
</ProjectName>
</Box>
<FiatPrice />
</Heading>
<Box direction="row">
<InfoButton />
<ConfigureButton />
</Box>
</BaseContainer>
</HeaderLine>
);
}

@ -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) => (
<div>
<Tip
dropProps={{ align: { left: "right" } }}
content={<TipContent message={blockPropertyDescriptions[e.key]} />}
plain
>
<span>
<CircleQuestion size="small" />
</span>
</Tip>
&nbsp;{blockPropertyDisplayNames[e.key] || e.key}
</div>
),
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<BlockDetailsProps> = ({
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" ? (
<Text size={"small"}>{blockNumber}</Text>
) : (
blockDisplayValues(block, key, (block as any)[key])
);
arr.push({ key, value } as tableEntry);
return arr;
}, [] as tableEntry[]);
return (
<>
<Box
flex
align="stretch"
justify="start"
margin={{ top: "-42px" }}
style={{ overflow: "auto" }}
>
<DataTable
className={"g-table-body-last-col-right"}
style={{ width: "100%", minWidth: "698px" }}
columns={columns}
data={blockData}
step={10}
border={{
header: {
color: "none",
},
body: {
color: "border",
side: "top",
size: "1px",
},
}}
/>
<Box align="center" justify="center">
<Anchor
onClick={() => setShowDetails(!showDetails)}
margin={{ top: "medium" }}
>
{showDetails ? (
<>
Show less&nbsp;
<CaretUpFill size="small" />
</>
) : (
<>
Show more&nbsp;
<CaretDownFill size="small" />
</>
)}
</Anchor>
</Box>
</Box>
</>
);
};

@ -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<BlockDetailsProps> = ({ 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 <>
<Box flex align="start" justify="start">
<div>
<b>Block</b> #{block.number}
</div>
<DataTable
style={{ width: '100%' }}
columns={columns}
data={blockData}
step={10}
border={{
header: {
color: 'none'
},
body: {
color: 'border',
side: 'top',
size: '1px'
}
}}
/>
<Anchor onClick={() => setShowDetails(!showDetails)}>
{showDetails
? <>Show less&nbsp;
<CaretUpFill size="small" /></>
: <>Show more&nbsp;
<CaretDownFill size="small" /></>
}
</Anchor>
</Box>
</>
}

@ -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<string, string> = {
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<string, string> = {
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<string, number> = {
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) => (
<>
<BlockNumber number={value} />
&nbsp;
{value > 0 && (
<Link to={`/block/${+value - 1}`}>
<FormPreviousLink size="small" color="brand" />
</Link>
)}
<Link to={`/block/${+value + 1}`}>
<FormNextLink size="small" color="brand" />
</Link>
</>
),
transactions: (value: any[]) =>
value.length > 0
? value.map((tx) => (
<>
<CopyBtn
value={tx}
onClick={() =>
toaster.show({
message: () => (
<Box direction={"row"} align={"center"} pad={"small"}>
<Icon size={"small"} color={"headerText"} />
<Text size={"small"}>Copied to clipboard</Text>
</Box>
),
})
}
/>
&nbsp;
<TransactionHash key={tx} hash={tx} />
<br />
</>
))
: null,
stakingTransactions: (value: any[]) =>
value.length > 0
? value.map((tx) => (
<>
<CopyBtn
value={tx}
onClick={() =>
toaster.show({
message: () => (
<Box direction={"row"} align={"center"} pad={"small"}>
<Icon size={"small"} color={"headerText"} />
<Text size={"small"}>Copied to clipboard</Text>
</Box>
),
})
}
/>
&nbsp;
<TransactionHash link="staking-tx" key={tx} hash={tx} />
<br />
</>
))
: null,
miner: (value: any) => <Address address={value} />,
hash: (value: any) => <BlockHash hash={value} />,
parentHash: (value: any) => <BlockHash hash={value} />,
timestamp: (value: any) => <Timestamp timestamp={value} withRelative />,
gasUsed: (value: any, block: Block) => (
<span>
{formatNumber(+value)} ({+value / +block.gasLimit}%){" "}
</span>
),
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 (
<div>
{!["transactions", "stakingTransactions", "uncles", "nonce"].includes(
key
) &&
!["0x", "0", 0, null].includes(displayValue) &&
!["miner"].find((item) => item === key) && (
<>
<CopyBtn
value={value}
onClick={() =>
toaster.show({
message: () => (
<Box direction={"row"} align={"center"} pad={"small"}>
<Icon size={"small"} color={"headerText"} />
<Text size={"small"}>Copied to clipboard</Text>
</Box>
),
})
}
/>
&nbsp;
</>
)}
{displayValue || "—"}
</div>
);
};

@ -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<T = {}> {
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<T = {}> extends React.Component<
IDropdownProps<T>,
{ 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<HTMLElement>) => {
if (!(this.element && this.element.contains(e.target as Node))) {
this.setState({ ...this.state, isOpen: false });
}
};
onClickItem = (item: T, evt: React.MouseEvent<HTMLDivElement>) => {
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 ? (
<Fragment key={`${groupItem.groupBy}`}>
<Fragment>{groupItem.renderGroupItem()}</Fragment>
{items.map((item) => (
<DataItem
key={`${item[this.props.keyField]}`}
background={"backgroundDropdownItem"}
onClick={(evt) => this.onClickItem(item, evt)}
itemHeight={itemHeight}
style={{ ...itemStyles }}
>
{this.props.renderItem(item)}
</DataItem>
))}
</Fragment>
) : null;
});
}
render() {
const {
group = [],
searchable,
themeMode,
itemHeight = "47px",
itemStyles = {},
} = this.props;
return (
<DropdownWrapper
className={this.props.className}
ref={(element) => (this.element = element as HTMLDivElement)}
border={{ size: "xsmall", color: "border" }}
>
<Value
onClick={() => {
this.setState({ ...this.state, isOpen: !this.state.isOpen });
}}
direction={"row"}
flex
>
<Box flex>{this.props.renderValue(this.selectedValue)}</Box>
{this.state.isOpen ? (
<CaretUpFill
onClick={(e) => {
console.log('CLICK')
e.stopPropagation()
this.setState({ ...this.state, isOpen: false });
}}
/>
) : (
<CaretDownFill
onClick={(e) => {
e.stopPropagation()
this.setState({ ...this.state, isOpen: true });
}}
/>
)}
</Value>
{this.state.isOpen ? (
<DataList
pad="xsmall"
background="background"
border={{ size: "xsmall", color: "border" }}
style={{ borderRadius: "0px" }}
>
{searchable ? (
<TextInput
value={this.state.searchText}
onChange={(evt: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
...this.state,
searchText: evt.currentTarget.value,
});
}}
color="red"
icon={<Search color="brand" />}
style={{
backgroundColor:
themeMode === "light" ? "white" : "transparent",
fontWeight: 500,
}}
placeholder="Search by symbol, token address"
/>
) : null}
{group.length
? this.renderGroupItems()
: this.props.items.map((item) => (
<DataItem
key={`${item[this.props.keyField]}`}
onClick={(evt) => this.onClickItem(item, evt)}
itemHeight={itemHeight}
style={{ ...itemStyles }}
>
{this.props.renderItem(item)}
</DataItem>
))}
</DataList>
) : null}
</DropdownWrapper>
);
}
}

@ -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 (
<BasePage
direction="row"
justify="between"
wrap={isLessLaptop}
margin={{ bottom: "medium" }}
>
<Box
justify="between"
pad={{ right: isLessMobileM ? "0" : "medium" }}
border={{
size: isLessMobileM ? "0" : "xsmall",
side: "right",
color: "border",
}}
style={{
height: isLessMobileM ? "auto" : "140px",
flex: isLessLaptop ? "1 1 50%" : "1 1 100%",
}}
gap={isLessMobileM ? "small" : "0"}
>
<ONEPrice />
{!isLessMobileM && <Line horizontal />}
<TransactionsCount />
</Box>
<Box
justify="between"
pad={{ left: "medium", right: isLessLaptop ? "0" : "medium" }}
border={{
size: isLessLaptop ? "0" : "xsmall",
side: "right",
color: "border",
}}
style={{
height: isLessMobileM ? "auto" : "140px",
flex: isLessLaptop ? "1 1 50%" : "1 1 100%",
}}
>
<ShardCount />
{!isLessMobileM && <Line horizontal />}
<BlockLatency latency={params.latency} />
</Box>
{isLessLaptop && (
<Line
horizontal
style={{ marginTop: isLessTablet ? "16px" : "24px" }}
/>
)}
<Box
justify="between"
pad={{
left: isLessLaptop ? "0" : "medium",
}}
margin={{ top: isLessLaptop ? "medium" : "0" }}
style={{ height: "140px", flex: "1 1 100%" }}
>
<BlockTransactionsHistory />
</Box>
</BasePage>
);
};
function ONEPrice() {
const { lastPrice = 0, priceChangePercent = 0 } = useONEExchangeRate();
return (
<Box direction="row" align="stretch">
<Box
pad={{ left: "xsmall", right: "small" }}
justify="center"
align="center"
>
<LineChart size="32px" color="brand" />
</Box>
<Box align="start">
<Text size="small" color="minorText">
{"ONE PRICE"}
</Text>
<Box direction="row" gap="xsmall" align="baseline">
<Text size="small" weight="bold">
$ {(+lastPrice).toFixed(2)}
</Text>
<Text
size="11px"
weight="bold"
color={priceChangePercent > 0 ? "status-ok" : "#d23540"}
>
({priceChangePercent > 0 ? "+" : ""}
{formatNumber(priceChangePercent)}%)
</Text>
</Box>
</Box>
</Box>
);
}
function TransactionsCount() {
const [count, setCount] = useState<string>("");
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 (
<Box direction="row" align="stretch">
<Box
pad={{ left: "xsmall", right: "small" }}
justify="center"
align="center"
>
<Transaction size="32px" color="brand" />
</Box>
<Box align="start">
<Text size="small" color="minorText">
{"TRANSACTIONS COUNT"}
</Text>
<Text size="small" weight="bold">
{formatNumber(+count)}
</Text>
</Box>
</Box>
);
}
function ShardCount() {
const count = process.env.REACT_APP_AVAILABLE_SHARDS?.split(",").length || 0;
return (
<Box direction="row" align="stretch">
<Box
pad={{ left: "xsmall", right: "small" }}
justify="center"
align="center"
>
<Cubes size="32px" color="brand" />
</Box>
<Box align="start">
<Text size="small" color="minorText">
{"SHARD COUNT"}
</Text>
<Text size="small" weight="bold">
{formatNumber(count)}
</Text>
</Box>
</Box>
);
}
function BlockLatency(params: { latency: number }) {
return (
<Box direction="row" align="stretch">
<Box
pad={{ left: "xsmall", right: "small" }}
justify="center"
align="center"
>
<LatencyIcon size="30px" color="brand" />
</Box>
<Box align="start">
<Text size="small" color="minorText">
{"BLOCK LATENCY"}
</Text>
<Text size="small" weight="bold">
{params.latency.toFixed(2)}s
</Text>
</Box>
</Box>
);
}
interface TxHitoryItem {
timestamp: string;
count: string;
}
function BlockTransactionsHistory() {
const [result, setResult] = useState<TxHitoryItem[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(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 (
<Box>
<Text size="small" color="minorText" style={{ flex: "1 0 auto" }}>
{"TRANSACTION HISTORY"}
</Text>
<Box style={{ flex: "1 1 100%", marginTop: "30px" }}>
{isLoading && (
<Box justify="center" align="center" height="110px">
<Spinner />
</Box>
)}
{!isLoading && (
<DataChart
data={data}
detail
axis={{
x: {
granularity: "medium",
property: "date",
},
y: {
granularity: "medium",
property: "count",
},
}}
series={[
{
property: "date",
label: "Date",
render: (value) => (
<Text size="xsmall" color="minorText">
{value}
</Text>
),
},
{
property: "count",
label: "Transactions",
render: (value) => (
<Text size="xsmall" color="minorText">
{formatNumber(value)}
</Text>
),
},
]}
size="fill"
chart={[
{
property: "count",
type: "bar",
color: "brand",
opacity: "medium",
thickness: "small",
},
]}
/>
)}
</Box>
</Box>
);
}
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};
`;

@ -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 (
<Box direction={"row"}>
<FormPrevious
style={{
opacity: props.disablePrev ? 0.7 : 1,
cursor: props.disablePrev ? "not-allowed" : "pointer",
marginRight: "10px",
userSelect: "none",
}}
onClick={props.disablePrev ? undefined : () => props.onClickPrev()}
/>
<FormNext
style={{
opacity: props.disableNext ? 0.7 : 1,
cursor: props.disableNext ? "not-allowed" : "pointer",
userSelect: "none",
}}
onClick={props.disableNext ? undefined : () => props.onClickNext()}
/>
</Box>
);
}

@ -0,0 +1,85 @@
import { DataTable, DataTableExtendedProps } from "grommet";
import React from "react";
import styled from "styled-components";
export interface ITableComponentProps {
tableProps: DataTableExtendedProps<any>;
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<ITableComponentProps> {
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 (
<Flex
ref={(element: any) => (this.element = element)}
className={this.props.className}
>
<DataTable
{...(this.props.tableProps as any)}
onMore={
this.props.alwaysOpenedRowDetails
? () => {
if (this.props.tableProps.onMore) {
this.props.tableProps.onMore();
}
this.clickExpandButtons();
}
: undefined
}
/>
</Flex>
);
}
}

@ -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: (
<Text color="minorText" size="small" style={{ fontWeight: 300 }}>
Shard
</Text>
),
render: (data: RPCTransactionHarmony) => (
<Box direction="row" gap="3px" align="center">
<Text size="small">{data.shardID}</Text>
<FormNextLink
size="small"
color="brand"
style={{ marginBottom: "2px" }}
/>
<Text size="small">{data.toShardID}</Text>
</Box>
),
},
{
property: "hash",
size: "xsmall",
resizeable: false,
header: (
<Text color="minorText" size="small" style={{ fontWeight: 300 }}>
Hash
</Text>
),
render: (data: RPCTransactionHarmony) => (
<Text
size="small"
style={{ cursor: "pointer" }}
onClick={() => {
history.push(`/tx/${data.hash}`);
}}
color="brand"
>
<Address address={data.hash} isShort />
</Text>
),
},
{
property: "block_number",
size: "260px",
resizeable: false,
header: (
<Text color="minorText" size="small" style={{ fontWeight: 300 }}>
Block number
</Text>
),
render: (data: RPCTransactionHarmony) => {
return (
<Text
size="small"
style={{ cursor: "pointer" }}
onClick={() => {
history.push(`/block/${data.blockNumber}`);
}}
color="brand"
>
{formatNumber(+data.blockNumber)}
</Text>
);
},
},
{
property: "from",
size: "large",
resizeable: false,
header: (
<Text color="minorText" size="small" style={{ fontWeight: 300 }}>
From
</Text>
),
render: (data: RPCTransactionHarmony) => <Address address={data.from} />,
},
{
property: "to",
size: "large",
resizeable: false,
header: (
<Text color="minorText" size="small" style={{ fontWeight: 300 }}>
To
</Text>
),
render: (data: RPCTransactionHarmony) => <Address address={data.to} />,
},
{
property: "value",
size: "380px",
resizeable: false,
header: (
<Text color="minorText" size="small" style={{ fontWeight: 300 }}>
ONEValue
</Text>
),
render: (data: RPCTransactionHarmony) => (
<Box justify="center">
<ONEValue value={data.value} timestamp={data.timestamp} />
</Box>
),
},
{
property: "timestamp",
size: "280px",
resizeable: false,
header: (
<Text color="minorText" size="small" style={{ fontWeight: 300 }}>
Timestamp
</Text>
),
render: (data: RPCTransactionHarmony) => (
<Box direction="row" gap="xsmall" justify="end">
{/*<Text size="small">*/}
{/* {dayjs(data.timestamp).format("YYYY-MM-DD, HH:mm:ss")},*/}
{/*</Text>*/}
<RelativeTimer
date={data.timestamp}
updateInterval={1000}
style={{ minWidth: "auto" }}
/>
</Box>
),
},
];
}
interface TransactionTableProps {
rowDetails?: (row: any) => JSX.Element;
data: any[];
columns?: ColumnConfig<any>[];
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 (
<>
<Box
direction="row"
justify={hidePagination ? "start" : "between"}
pad={{ bottom: "small" }}
margin={{ bottom: "small" }}
border={{ size: "xsmall", side: "bottom", color: "border" }}
>
{!hideCounter ? (
<Text style={{ flex: "1 1 100%" }}>
<b>{Math.min(limit, data.length)}</b> transaction
{data.length !== 1 ? "s" : ""} shown
</Text>
) : (
<Box />
)}
{!hidePagination && (
<PaginationNavigator
onChange={setFilter}
isLoading={isLoading}
filter={filter}
totalElements={totalElements}
elements={data}
noScrollTop={noScrollTop}
property="block_number"
/>
)}
</Box>
<Box
style={{
overflow: "auto",
opacity: _IsLoading ? "0.4" : "1",
transition: "0.1s all",
minHeight: "600px",
}}
>
{_IsLoading ? (
<Box align={"center"} justify={"center"} flex>
<Spinner size={"large"} />
</Box>
) : !data.length && !_IsLoading ? (
<Box style={{ height: "120px" }} justify="center" align="center">
<Text size="small">{emptyText}</Text>
</Box>
) : (
<TableComponent
alwaysOpenedRowDetails={props.rowDetails ? true : false}
tableProps={{
className: "g-table-header",
style: { width: "100%", minWidth },
columns: columns ? columns : getColumns({ history }),
data: data,
step,
primaryKey: props.primaryKey ? props.primaryKey : undefined,
border: {
header: {
color: "brand",
},
body: {
color: "border",
side: "top",
size: "1px",
},
},
rowDetails: props.rowDetails
? (row: any) => (
<div style={{ textAlign: "left" }}>
{props.rowDetails && props.rowDetails(row)}
</div>
)
: undefined,
}}
/>
)}
</Box>
{!hidePagination && (
<Box
direction="row"
justify="between"
align="center"
margin={{ top: "medium" }}
>
<PaginationRecordsPerPage filter={filter} onChange={setFilter} />
<PaginationNavigator
onChange={setFilter}
isLoading={isLoading}
filter={filter}
totalElements={totalElements}
elements={data}
noScrollTop={noScrollTop}
property="block_number"
/>
</Box>
)}
</>
);
}

@ -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<Filter>(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 (
<Box margin={{ top: "medium" }}>
<TransactionsTable
columns={getColumns({ timestamp })}
data={data.sort((a, b) => (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) => (
<DisplaySignatureMethod
internalTransaction={row}
key={`${row.from}_${row.to}`}
/>
)}
/>
</Box>
);
}
function getColumns(props?: any) {
const { timestamp } = props;
return [
{
property: "type",
header: (
<Text color="minorText" size="small" style={{ fontWeight: 300 }}>
Type
</Text>
),
render: (data: InternalTransaction) => (
<Text size="small">
<TransactionType type={data.type} />
</Text>
),
},
/* {
property: "method",
header: (
<Text color="minorText" size="small" style={{ fontWeight: 300 }}>
Suggested Method
</Text>
),
render: (data: InternalTransaction) => {
let signature;
try {
// @ts-ignore
signature =
data.signatures &&
data.signatures.map((s) => s.signature)[0].split("(")[0];
} catch (err) {}
return <Text size="small">{signature || "—"}</Text>;
},
},*/
{
property: "from",
header: (
<Text color="minorText" size="small" style={{ fontWeight: 300 }}>
From
</Text>
),
render: (data: InternalTransaction) => (
<Text size="small">
<Address address={data.from} />
</Text>
),
},
{
property: "to",
header: (
<Text color="minorText" size="small" style={{ fontWeight: 300 }}>
To
</Text>
),
render: (data: InternalTransaction) => (
<Text size="small">
<Address address={data.to} />
</Text>
),
},
{
property: "value",
header: (
<Text color="minorText" size="small" style={{ fontWeight: 300 }}>
ONEValue
</Text>
),
render: (data: InternalTransaction) => (
<Box justify="center" align="end">
<ONEValue value={data.value} timestamp={timestamp} />
</Box>
),
},
];
}

@ -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) => (
<div>
<Tip
dropProps={{ align: { left: "right" } }}
content={
<TipContent
message={
transactionPropertyDescriptions[e.key + type] ||
transactionPropertyDescriptions[e.key]
}
/>
}
plain
>
<span>
<CircleQuestion size="small" />
</span>
</Tip>
&nbsp;
{transactionPropertyDisplayNames[e.key + type] ||
transactionPropertyDisplayNames[e.key] ||
e.key}
</div>
),
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 (
<Box
direction={"column"}
align={"start"}
pad={"xxsmall"}
style={{ borderRadius: "6px", marginBottom: "3px" }}
>
<Box direction={"row"}>
<Text size="small" color="minorText">
From :&nbsp;
</Text>
<Address address={e.parsed["$0"].toLowerCase()} />
&nbsp;
<Text size="small" color="minorText">
To :&nbsp;
</Text>
<Address address={e.parsed["$1"].toLowerCase()} />
</Box>
<Box align={"center"} direction={"row"}>
<Text size="small" color="minorText">
Value : &nbsp;
</Text>
<TokenValueBalanced
value={val}
tokenAddress={address}
direction={"row"}
/>
</Box>
</Box>
);
})}
</>
);
};
export const TransactionDetails: FunctionComponent<TransactionDetailsProps> = ({
transaction,
type,
logs = [],
errorMsg,
}) => {
const [showDetails, setShowDetails] = useState(false);
const newTransaction = {
Status:
errorMsg === undefined ? (
+transaction.shardID > 0 ? (
<TxStatusComponent msg={''} />
) : (
<> </>
)
) : (
<TxStatusComponent msg={errorMsg} />
),
...transaction,
tokenTransfers: tokenTransfers(logs),
gasPrice: <Box justify="center">{CalculateFee(transaction)}</Box>,
};
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 (
<>
<Box flex align="start" justify="start" style={{ overflow: "auto" }}>
<DataTable
className={"g-table-body-last-col-right g-table-no-header"}
style={{ width: "100%", minWidth: "698px" }}
columns={getColumns({ type })}
data={txData}
step={10}
border={{
header: {
color: "none",
},
body: {
color: "border",
side: "top",
size: "1px",
},
}}
/>
</Box>
<Box align="center" justify="center" style={{ userSelect: "none" }}>
<Anchor
onClick={() => setShowDetails(!showDetails)}
margin={{ top: "medium" }}
>
{showDetails ? (
<>
Show less&nbsp;
<CaretUpFill size="small" />
</>
) : (
<>
Show more&nbsp;
<CaretDownFill size="small" />
</>
)}
</Anchor>
</Box>
</>
);
};

@ -0,0 +1 @@
export const todo = () => {}

@ -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 (
<Box style={{ height: '120px' }} justify="center" align="center">
<Text size="small">
No Logs for <b>{hash}</b>
</Text>
</Box>
)
}
return (
<Box margin={{ top: 'medium' }}>
{logs
.sort((a, b) => a.logIndex - b.logIndex)
.map((log, i) => (
<LogItem key={i} log={log} />
))}
</Box>
)
}
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 (
<Box
gap="small"
border={{ size: 'xsmall', side: 'bottom', color: 'border' }}
pad={{ bottom: 'small' }}
>
<Box>
<Text color="minorText" size="small">
Address
</Text>
<Text size="small" color="brand">
<Address address={address} style={{ wordBreak: 'break-all' }} />
</Text>
</Box>
{signatures && signatures.length ?
<Box>
<Text color="minorText" size="small">
Suggested Event
</Text>
<Text size="small" color="black">
{displaySignature || signatures[0].signature || ''}
</Text>
</Box>
: null}
<Box>
<Text color="minorText" size="small">
Topics
</Text>
<Box gap="xxsmall">
{topics.map(((topic, i) => (
<Text size="small" color="brand" style={{ wordBreak: 'break-all' }}>
{topic}{i !== topics.length - 1 ? ', ' : ''}
</Text>
)))}
</Box>
</Box>
<Box>
<Text color="minorText" size="small">
Data
</Text>
<Text size="small" color="brand" style={{ wordBreak: 'break-all' }}>
{data}
</Text>
</Box>
</Box>
)
}

@ -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<string, string> = {
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<string, number> = {
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<string, string> = {
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) => <BlockNumber number={value} hash={data['blockHash']} />,
from: (value: any) => <Address address={value} />,
value: (value: any, tx: any) => (
<ONEValue value={value} timestamp={tx.timestamp} />
),
to: (value: any) => <Address address={value} />,
hash: (value: any) => <TransactionHash hash={value} />,
hash__staking: (value: any) => (
<TransactionHash hash={value} link="staking-tx" />
),
hash_harmony: (value: any) => <TransactionHash hash={value} />,
blockHash: (value: any) => <BlockHash hash={value} />,
timestamp: (value: any) => <Timestamp timestamp={value} withRelative />,
gasUsed: (value: any, tx: RPCTransactionHarmony) => (
<span>
{value} ({+value / +tx.gas}%){" "}
</span>
),
shardID: (value: any, tx: RPCTransactionHarmony) => (
<span>
{value}
<FormNextLink size="small" color="brand" />
{tx.toShardID}
</span>
),
type: (value: any) => <StakingTransactionTypeValue type={value} />,
amount: (value: any, tx: any) => (
<ONEValue value={value} timestamp={tx.timestamp} />
),
name: (value: any) => <span>{value}</span>,
delegatorAddress: (value: any) => <Address address={value} />,
validatorAddress: (value: any) => <Address address={value} />,
commissionRate: (value: any) => <span>{value}</span>,
maxCommissionRate: (value: any) => <span>{value}</span>,
maxChangeRate: (value: any) => <span>{value}</span>,
minSelfDelegation: (value: any) => <span>{value}</span>,
maxTotalDelegation: (value: any) => <span>{value}</span>,
website: (value: any) => <a href={value}>{value}</a>,
identity: (value: any) => <span>{value}</span>,
securityContact: (value: any) => <span>{value}</span>,
details: (value: any) => <span>{value}</span>,
slotPubKeys: (value: any) => <span>{value}</span>,
slotPubKeyToAdd: (value: any) => <span>{value}</span>,
slotPubKeyToRemove: (value: any) => <span>{value}</span>,
tokenTransfers: (value: any) => <span>{value}</span>,
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 (
<Box direction="row" align="baseline">
{!["shardID"].includes(key) && ![0, "0", "—"].includes(displayValue) && (
<>
{copyText ? (
<CopyBtn
value={copyText}
onClick={() =>
toaster.show({
message: () => (
<Box direction={"row"} align={"center"} pad={"small"}>
<Icon size={"small"} color={"headerText"} />
<Text size={"small"}>Copied to clipboard</Text>
</Box>
),
})
}
/>
) : null}
&nbsp;
</>
)}
{displayValue}
</Box>
);
};

@ -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 (
<div style={{ display: "inline-block" }}>
<Box direction={"row"} align={"center"} justify={"start"}>
<CopyBtn
value={outPutAddress}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toaster.show({
message: () => (
<Box direction={"row"} align={"center"} pad={"small"}>
<Icon size={"small"} color={"headerText"} />
<Text size={"small"}>Copied to clipboard</Text>
</Box>
),
});
}}
/>
<Text
size="small"
color={color}
style={{
marginLeft: "7px",
cursor: "pointer",
textDecoration:
address === EMPTY_ADDRESS
? "none"
: !!parsedName
? "underline"
: "none",
...style,
}}
onClick={
address === EMPTY_ADDRESS
? undefined
: props.noHistoryPush
? undefined
: () => history.push(`/${type}/${address}`)
}
>
{parsedName ||
(isShort
? `${outPutAddress.substr(0, 4)}...${outPutAddress.substr(-4)}`
: outPutAddress)}
</Text>
</Box>
</div>
);
};

@ -0,0 +1 @@
export const Amount = () => {}

@ -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<AnchorLinkProps> = props => {
return <Anchor
as={({ colorProp, hasIcon, hasLabel, focus, ...p }) => <Link {...p} />}
{...props}
/>
}
export type AnchorLinkProps = LinkProps &
AnchorProps &
Omit<JSX.IntrinsicElements['a'], 'color'>

@ -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 (
<Box
pad={{ horizontal: isLessTablet ? "12px" : '20px' }}
{...props}
style={{ ...sizes, width: "100%", flex: "1 1 auto", ...style }}
/>
);
};
export const BasePage = (props: any) => {
const { style } = props;
return (
<Box
pad="medium"
background="background"
border={{ size: "xsmall", color: "border" }}
{...props}
style={{
borderRadius: "8px",
overflow: 'hidden',
...style,
}}
/>
);
};

@ -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 <AnchorLink to={link} label={hash} style={{fontWeight: 400}} />
}

@ -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 (
<AnchorLink
to={link}
label={formatNumber(+number, {})}
style={{ fontWeight: 400 }}
/>
);
};

@ -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 <StyledButton {...props} />;
}
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;
}
`;

@ -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<SVGSVGElement>) => void;
}) {
return (
<Clone
size="small"
color="brand"
onClick={(e) => {
copyText(props.value);
if (props.onClick) {
props.onClick(e);
}
}}
style={{ cursor: "pointer" }}
/>
);
}

@ -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 (
<Box style={{ marginLeft: "15px" }}>
{isLoading ? (
<Loader>
<Box align={"center"} justify={"center"} flex height={"100%"}>
<Spinner />
</Box>
</Loader>
) : null}
{isErrorLoading ? (
<ErrorPreview direction={"column"} justify={"center"} align={"center"}>
<Image
size={"medium"}
style={{ marginBottom: "10px", marginTop: "10px" }}
/>
</ErrorPreview>
) : url ? (
<InventImg
src={url}
onLoad={() => setIsLoading(false)}
onError={() => {
setIsLoading(false);
setIsErrorLoading(true);
}}
/>
) : (
<EmptyImage
direction={"column"}
justify={"center"}
align={"center"}
></EmptyImage>
)}
</Box>
);
}

@ -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 <Text>{value}</Text>;
}
return (
<Box direction="column">
<Text size="small" style={{ wordBreak: "break-all", maxHeight: '40vh', overflowY: 'auto' }}>
{isFull ? value : `${value.substr(0, 62)}...`}
</Text>
<Text
size="small"
color="brand"
style={{
flex: "0 0 auto",
cursor: "pointer",
textDecoration: "underline",
}}
onClick={() => setIsFull(!isFull)}
>
{isFull ? "show less" : "show full"}
</Text>
</Box>
);
};

@ -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 <Text size="xsmall">&nbsp;</Text>;
}
const price = parseFloat(lastPrice).toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
currency: "USD",
});
const change = (+priceChangePercent).toFixed(2);
const isPositive = +priceChangePercent >= 0;
return (
<>
<Text size="xsmall">ONE:&nbsp;${price}&nbsp;</Text>
<Text size="xsmall" color={isPositive ? "#69FABD" : "status-error"}>
({isPositive && "+"}
{change}%)
</Text>
</>
);
};

@ -0,0 +1 @@
export const HexData = () => {}

@ -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 (
<Box direction="row" gap="xsmall">
<Text
weight={v > 0 ? "bold" : "normal"}
size="small"
margin={{ right: "xxmall" }}
>
{v.toString()} ONE
</Text>
{USDValue && +price > 0 && !isTodayTransaction && !hideTip && (
<Tip
dropProps={{ align: { left: "right" }, margin: { left: "small" } }}
content={
<TipContent
message={
<span>
{`Displaying value on ${dayjs(timestamp).format(
"YYYY-MM-DD"
)}. Current value`}{" "}
<b>
$
{formatNumber(v * +lastPrice, {
maximumFractionDigits: 2,
})}
</b>
</span>
}
/>
}
plain
>
<Text size="small">(${USDValue})</Text>
</Tip>
)}
{USDValue && +price > 0 && isTodayTransaction && (
<Text size="small">(${USDValue})</Text>
)}
</Box>
);
};

@ -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 (
<Dropdown
items={normilizedValue}
keyField={"value"}
themeMode={themeMode}
itemHeight={"30px"}
itemStyles={{ justifyContent: "center" }}
renderValue={() => (
<Box direction={"row"} align={"center"} style={{ paddingTop: "2px" }}>
<Text size={"small"}>
<b>
{normilizedValue.reduce((prev, cur) => {
prev += cur.one;
return prev;
}, 0)}{" "}
ONE
</b>
</Text>
<Text size={"small"} style={{ paddingLeft: "4px" }}>
($
{normilizedValue
.reduce((prev, cur) => {
prev += +cur.usd;
return prev;
}, 0)
.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
currency: "USD",
})}
)
</Text>
</Box>
)}
renderItem={(item) => (
<Box direction={"row"}>
<Text size={"small"} style={{ width: "52.5px" }}>
Shard {item.index}:{" "}
</Text>
<Text size={"small"} style={{ paddingLeft: "4px" }}>
<b>{item.one} ONE </b>
</Text>
{item.usd ? (
<Text size={"small"} style={{ paddingLeft: "4px" }}>
($
{item.usd.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
currency: "USD",
})}
)
</Text>
) : null}
</Box>
)}
/>
);
};

@ -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 (
<Box style={{ flex: "0 0 auto" }}>
<Pagination
//@ts-ignore
currentPage={+(+offset / limit).toFixed(0) + 1}
totalPages={+Math.ceil(Number(totalElements) / limit).toFixed(0)}
onPrevPageClick={onPrevClick}
onNextPageClick={onNextClick}
showPages={showPages}
disableNextBtn={elements.length < limit}
disablePrevBtn={filter.offset === 0}
/>
</Box>
);
}
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 (
<Box direction="row" gap="small">
<FormPrevious
onClick={disablePrevBtn ? undefined : onPrevPageClick}
style={{
cursor: "pointer",
userSelect: "none",
opacity: disablePrevBtn ? 0.5 : 1,
}}
/>
{showPages && (
<Text style={{ fontWeight: "bold" }}>{formatNumber(+currentPage)}</Text>
)}
{showPages && <Text style={{ fontWeight: 300 }}>/</Text>}
{showPages && (
<Text style={{ fontWeight: 300 }}>{formatNumber(+totalPages)}</Text>
)}
<FormNext
onClick={disableNextBtn ? undefined : onNextPageClick}
style={{
cursor: "pointer",
userSelect: "none",
opacity: disableNextBtn ? 0.5 : 1,
}}
/>
</Box>
);
}
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 (
<Box direction="row" gap="small" align="center">
<Box style={{ width: "95px" }}>
<Select
options={options}
value={limit.toString()}
onChange={onChangeLimit}
/>
</Box>
<Text size="small">records per page</Text>
</Box>
);
}

@ -0,0 +1,120 @@
import React, { useEffect } from "react";
import { Box } from "grommet";
import { Filter } from "src/types";
import { FormPrevious, FormNext } from "grommet-icons";
interface PaginationNavigator {
filter: Filter;
blocks: any[];
totalElements: number;
onChange: (filter: Filter) => void;
property: string;
}
export function PaginationBlockNavigator(props: PaginationNavigator) {
const { blocks, totalElements, filter, onChange, property } = props;
const { filters, limit = 10 } = filter;
const { value } = filters[0];
useEffect(() => {
const scrollBody = document.getElementById("scrollBody");
if (scrollBody) {
scrollBody.scrollTo({ top: 0 });
}
}, [filter]);
const blockNumbers = blocks.map((b) => +b.number);
const minBlockNumber = blockNumbers.reduce(
(a, b) => (a === -1 || a > b ? b : a),
-1
);
const maxBlockNumber = blockNumbers.reduce((a, b) => Math.max(a, b), 0);
const onPrevClick = () => {
const newFilter = JSON.parse(JSON.stringify(filter)) as Filter;
const innerFilter = newFilter.filters.find((i) => i.property === property);
if (innerFilter) {
innerFilter.type = "lt";
innerFilter.value = maxBlockNumber + limit + 1;
}
onChange(newFilter);
};
const onNextClick = () => {
const newFilter = JSON.parse(JSON.stringify(filter)) as Filter;
const innerFilter = newFilter.filters.find((i) => i.property === property);
if (innerFilter) {
innerFilter.type = "lt";
innerFilter.value = minBlockNumber;
}
onChange(newFilter);
};
return (
<Box style={{ flex: "1 0 auto" }}>
<Pagination
currentPage={+((totalElements - +value) / limit).toFixed(0) + 1}
totalPages={+(Number(totalElements) / limit).toFixed(0)}
onPrevPageClick={onPrevClick}
onNextPageClick={onNextClick}
/>
</Box>
);
}
interface PaginationProps {
currentPage: number;
totalPages: number;
onPrevPageClick: () => void;
onNextPageClick: () => void;
}
function Pagination(props: PaginationProps) {
const { onPrevPageClick, onNextPageClick } = props;
return (
<Box direction="row" gap="small" justify="end">
<FormPrevious
onClick={onPrevPageClick}
style={{ cursor: "pointer", userSelect: "none" }}
/>
<FormNext
onClick={onNextPageClick}
style={{ cursor: "pointer", userSelect: "none" }}
/>
</Box>
);
}
// interface ElementsPerPage {
// filter: Filter;
// onChange: (filter: Filter) => void;
// options?: number[];
// }
//
// const defaultOptions: string[] = ["10", "25", "50", "100"];
// export function PaginationBlockRecordsPerPage(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 (
// <Box direction="row" gap="small" align="center">
// <Box style={{ width: "95px" }}>
// <Select
// options={options}
// value={limit.toString()}
// onChange={onChangeLimit}
// />
// </Box>
// <Text size="small">records per page</Text>
// </Box>
// );
// }

@ -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 <div>{render(formattedValue)}</div>;
}
return (
<Text
size="small"
style={{ minWidth: "125px", ...style }}
color="minorText"
>
{formattedValue}
</Text>
);
}

@ -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<any[]>([]);
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 (
<div style={style}>
<Box
key={`${results[index].item.address}_${results[index].type}`}
direction={"row"}
pad={"xsmall"}
style={{
cursor: "pointer",
minHeight: "40px",
borderStyle: "solid",
borderBottomWidth: "1px",
borderTopWidth: "0px",
borderLeftWidth: "0px",
borderRightWidth: "0px",
paddingLeft: "10px",
}}
align={"center"}
border={{
color: "backgroundBack",
size: "xsmall",
}}
onClick={() => {
history.push(`/address/${results[index].item.address}`);
setValue("");
}}
>
<Text size={"small"} style={{ paddingRight: "5px" }}>
Name {results[index].name} |
</Text>
<Text size={"small"} style={{ paddingRight: "5px" }}>
Symbol {results[index].symbol} |
</Text>
<Address
address={results[index].item.address}
noHistoryPush
displayHash
/>
</Box>
</div>
);
};
return (
<Box
width="100%"
pad={{ vertical: "medium" }}
style={{ position: "relative" }}
>
<TextInput
value={value}
onChange={onChange}
onFocus={() => setFocus(true)}
onBlur={() => {
setTimeout(() => {
setFocus(false);
}, 100);
}}
onKeyDown={(e) => {
if (e.keyCode === 13) {
onChange(e);
}
}}
color="red"
icon={<Search color="brand" />}
style={{
backgroundColor: themeMode === "light" ? "white" : "transparent",
fontWeight: 500,
}}
placeholder="Search by Address / Transaction Hash / Block / Token"
/>
{focus && results.length && value ? (
<Box
style={{
borderRadius: "6px",
position: "absolute",
marginTop: "43px",
width: "100%",
zIndex: 9,
maxHeight: "350px",
minHeight: "350px",
overflowY: "auto",
overflowX: "hidden",
boxShadow:
themeMode === "light"
? "0 0 10px 1px rgba(0,0,0,0.05)"
: "0 0 10px 1px rgba(255,255,255,0.09)",
}}
background={"background"}
>
<Box height={"40px"} pad={"small"}>
<Text size={"small"}>
<b>{results.length}</b> found
</Text>
</Box>
<AutoSizer>
{({ height, width }) => (
<List
className="List"
height={height}
itemCount={results.length}
itemSize={40}
width={width}
>
{Row}
</List>
)}
</AutoSizer>
</Box>
) : null}
</Box>
);
};

@ -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 (
<Dropdown
themeMode={themeMode}
itemHeight={"30px"}
items={
process.env.REACT_APP_AVAILABLE_SHARDS?.split(",").map((item) => ({
value: item,
})) || []
}
renderValue={(dataItem) => (
<Box
justify={"center"}
style={{ paddingTop: "2px" }}
>{`Shard ${dataItem.value}`}</Box>
)}
renderItem={(dataItem) => <>{`Shard ${dataItem.value}`}</>}
onClickItem={(item) => props.onClick(item.value)}
value={{ value: props.selected }}
itemStyles={{}}
keyField={"value"}
/>
);
}

@ -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 (
<Text size="small" margin={{ right: "xxmall" }}>
{typeMap[type] || type}
</Text>
);
};
const typeMap: Record<StakingTransactionType, string> = {
CreateValidator: "Create Validator",
EditValidator: "Edit Validator",
CollectRewards: "Collect Rewards",
Undelegate: "Undelegate",
Delegate: "Delegate",
};

@ -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 (
<span>
<Clock size="small" />
&nbsp;{dayjs(timestamp).format("YYYY-MM-DD, HH:mm:ss")}
{withRelative && <span>, <RelativeTimer date={timestamp} /></span>}
</span>
);
};

@ -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 <Text size="small"></Text>;
}
if (!value) {
return null;
}
const bi = Big(value).div(10 ** tokenInfo.decimals);
const v = formatNumber ? _formatNumber(bi.toNumber()) : bi.toString();
return (
<Text size="small" style={style}>
<b>{v}</b> {hideSymbol ? null : tokenInfo.symbol}
</Text>
);
};

@ -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<IPairPrice>({} 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 (
<Text size="small" style={style}>
<b>
{dollar && dollar.lastPrice ? (
<Box direction={direction}>
<Text size={"small"}>
{`${v}`}
<AnchorLink to={"/hrc20"} label={`${tokenInfo.symbol}`} />
</Text>
<Text size={"small"} style={{ paddingLeft: "0.3em" }}>
{`($${dollarPrice.toFixed(2).toString()})`}
</Text>
</Box>
) : (
<Text size={"small"}>
{`${v}`}{" "}
<AnchorLink
to={`/address/${tokenInfo.address}`}
label={`${tokenInfo.symbol}`}
/>
</Text>
)}
</b>
</Text>
);
};

@ -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 }) => (
<Box direction="row" align="center">
<Box background="background" direction="row" pad="small" round="xsmall" border={{color: 'border' }}>
<div>{message}</div>
</Box>
</Box>
)

@ -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 <AnchorLink to={url} label={hash} style={{ fontWeight: 400 }} />;
};

@ -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 (
<Text size="small" margin={{ right: "xxmall" }}>
{typeMap[type] || type}
</Text>
);
};
const typeMap: Record<string, string> = {
call: "Call",
staticcall: "Static Call",
create: "Create",
create2: "Create 2",
delegatecall: "Delegate Call",
};

@ -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 ? (
<Box direction={"row"} align={"center"}>
<Box
align={"center"}
direction={"row"}
background="backgroundError"
style={{ borderRadius: "6px", marginRight: "10px", padding: "3px 8px" }}
>
<CircleAlert color={"errorText"} size={"small"} />
<Text color={"errorText"} size={"small"} style={{ marginLeft: "5px" }}>
Error
</Text>
</Box>
<Text color={"errorText"} size={"xsmall"}>
{msg}
</Text>
</Box>
) : (
<Box
direction={"row"}
align={"center"}
background={"backgroundSuccess"}
style={{ borderRadius: "6px", marginRight: "10px", padding: "3px 8px" }}
>
<StatusGood color={"successText"} size={"small"} />
<Text color={"successText"} size={"small"} style={{ marginLeft: "5px" }}>
Success
</Text>
</Box>
);
}

@ -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 (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fillRule="evenodd"
clipRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="1.41421"
>
<path
fill={theme.global.colors[color] || color}
id="telegram-4"
d="M12,0c-6.626,0 -12,5.372 -12,12c0,6.627 5.374,12 12,12c6.627,0 12,-5.373 12,-12c0,-6.628 -5.373,-12 -12,-12Zm3.224,17.871c0.188,0.133 0.43,0.166 0.646,0.085c0.215,-0.082 0.374,-0.267 0.422,-0.491c0.507,-2.382 1.737,-8.412 2.198,-10.578c0.035,-0.164 -0.023,-0.334 -0.151,-0.443c-0.129,-0.109 -0.307,-0.14 -0.465,-0.082c-2.446,0.906 -9.979,3.732 -13.058,4.871c-0.195,0.073 -0.322,0.26 -0.316,0.467c0.007,0.206 0.146,0.385 0.346,0.445c1.381,0.413 3.193,0.988 3.193,0.988c0,0 0.847,2.558 1.288,3.858c0.056,0.164 0.184,0.292 0.352,0.336c0.169,0.044 0.348,-0.002 0.474,-0.121c0.709,-0.669 1.805,-1.704 1.805,-1.704c0,0 2.084,1.527 3.266,2.369Zm-6.423,-5.062l0.98,3.231l0.218,-2.046c0,0 3.783,-3.413 5.941,-5.358c0.063,-0.057 0.071,-0.153 0.019,-0.22c-0.052,-0.067 -0.148,-0.083 -0.219,-0.037c-2.5,1.596 -6.939,4.43 -6.939,4.43Z"
/>
</svg>
);
}
export function DiscordIcon(props: IIconProps) {
const theme = React.useContext(ThemeContext);
const { size = "24px", color = theme.global.palette.Grey } = props;
return (
<svg
width={size}
height={size}
enableBackground="new 0 0 512 512"
viewBox="0 0 512 512"
>
<circle
cx="256"
cy="256"
fill={theme.global.colors[color] || color}
id="ellipse"
r="256"
/>
<path
d="M372.4,168.7c0,0-33.3-26.1-72.7-29.1l-3.5,7.1c35.6,8.7,51.9,21.2,69,36.5 c-29.4-15-58.5-29.1-109.1-29.1s-79.7,14.1-109.1,29.1c17.1-15.3,36.5-29.2,69-36.5l-3.5-7.1c-41.3,3.9-72.7,29.1-72.7,29.1 s-37.2,54-43.6,160c37.5,43.3,94.5,43.6,94.5,43.6l11.9-15.9c-20.2-7-43.1-19.6-62.8-42.3c23.5,17.8,59.1,36.4,116.4,36.4 s92.8-18.5,116.4-36.4c-19.7,22.7-42.6,35.3-62.8,42.3l11.9,15.9c0,0,57-0.3,94.5-43.6C409.6,222.7,372.4,168.7,372.4,168.7z M208.7,299.6c-14.1,0-25.5-13-25.5-29.1s11.4-29.1,25.5-29.1c14.1,0,25.5,13,25.5,29.1S222.8,299.6,208.7,299.6z M303.3,299.6 c-14.1,0-25.5-13-25.5-29.1s11.4-29.1,25.5-29.1s25.5,13,25.5,29.1S317.3,299.6,303.3,299.6z"
fill={theme.global.colors.background}
/>
</svg>
);
}
export function LatencyIcon(props: IIconProps) {
const theme = React.useContext(ThemeContext);
const { size = "24px", color = theme.global.palette.Grey } = props;
return (
<svg
width={size}
height={size}
viewBox="0 0 512 512"
enableBackground="new 0 0 512 512;"
fill={theme.global.colors[color] || color}
>
<g>
<path
d="M256,512C114.5,512,0,397.5,0,256c0-69.8,27.9-135.9,78.2-184.3c8.4-8.4,21.4-7.4,28.9,0.9
c8.4,8.4,7.4,21.4-0.9,28.9C63.3,142.4,40,197.4,40,256c0,118.2,96.8,215,215,215s216-96.8,216-215c0-111.7-85.6-203.9-194.6-214.1
v80.1c0,11.2-9.3,20.5-20.5,20.5c-11.2,0-20.5-9.3-20.5-20.5V20.5C235.5,9.3,244.8,0,256,0c141.5,0,256,114.5,256,256
S397.5,512,256,512z"
/>
<path
d="M153.6,135.9l127.5,91.2c17.7,12.1,21.4,36.3,9.3,54s-36.3,21.4-54,9.3c-3.7-2.8-6.5-5.6-9.3-9.3
l-91.2-127.5c-3.7-5.6-2.8-14,2.8-17.7C143.4,132.2,148.9,132.2,153.6,135.9z"
/>
</g>
</svg>
);
}
// export function TransactionsIcon(props: IIconProps) {
// const theme = React.useContext(ThemeContext);
// const { size = "24px", color = theme.global.palette.Grey } = props;
//
// return (
// <svg
// width={size}
// height={size}
// viewBox="-16 0 480 480"
// fill={theme.global.colors[color] || color}
//
// >
// <path d="m440 224h-72v-184c-.027344-22.082031-17.917969-39.9726562-40-40h-264c-17.671875 0-32 14.328125-32 32v96h-24c-4.417969 0-8 3.582031-8 8v48c0 4.417969 3.582031 8 8 8h24v88c0 4.417969 3.582031 8 8 8h40v160c0 17.671875 14.328125 32 32 32h264c17.671875 0 32-14.328125 32-32s-14.328125-32-32-32h-8v-128h72c4.417969 0 8-3.582031 8-8v-48c0-4.417969-3.582031-8-8-8zm-392-192c0-8.835938 7.164062-16 16-16s16 7.164062 16 16v96h-32zm-32 112h224c4.023438 0 7.421875-2.984375 7.9375-6.976562l27.566406 22.976562-27.566406 22.976562c-.515625-3.992187-3.914062-6.976562-7.9375-6.976562h-224zm32 128v-80h32v80zm344 176c0 8.835938-7.164062 16-16 16h-236.296875c2.824219-4.859375 4.304687-10.378906 4.296875-16v-16h232c8.835938 0 16 7.164062 16 16zm-40-32h-216c-4.417969 0-8 3.582031-8 8v24c0 8.835938-7.164062 16-16 16s-16-7.164062-16-16v-256h136v8c0 3.105469 1.796875 5.933594 4.609375 7.25s6.132813.886719 8.519531-1.105469l48-40c1.820313-1.519531 2.875-3.769531 2.875-6.144531s-1.054687-4.625-2.875-6.144531l-48-40c-2.386718-1.992188-5.707031-2.421875-8.519531-1.105469s-4.609375 4.144531-4.609375 7.25v8h-136v-96c-.027344-5.632812-1.558594-11.15625-4.433594-16h236.433594c13.253906 0 24 10.746094 24 24v184h-136v-8c0-3.105469-1.796875-5.933594-4.609375-7.25s-6.132813-.886719-8.519531 1.105469l-48 40c-1.820313 1.519531-2.875 3.769531-2.875 6.144531s1.054687 4.625 2.875 6.144531l48 40c2.386718 1.992188 5.707031 2.421875 8.519531 1.105469s4.609375-4.144531 4.609375-7.25v-8h136zm80-144h-224c-1.03125.003906-2.050781.210938-3 .609375-.292969.152344-.574219.324219-.847656.511719-.574219.296875-1.113282.660156-1.601563 1.085937-.257812.289063-.496093.59375-.710937.914063-.390625.449218-.722656.941406-1 1.472656-.15625.375-.277344.765625-.367188 1.167969-.167968.390625-.300781.796875-.394531 1.214843l-27.582031-22.976562 27.566406-22.976562c.09375.417968.226562.824218.394531 1.214843.085938.402344.210938.792969.367188 1.167969.273437.53125.609375 1.023438 1 1.472656.214843.320313.453125.625.710937.914063.488282.425781 1.027344.789062 1.601563 1.085937.273437.1875.554687.359375.847656.511719.953125.402344 1.980469.609375 3.015625.609375h224zm0 0" />
// <path d="m128 48h80v16h-80zm0 0" />
// <path d="m128 80h80v16h-80zm0 0" />
// <path d="m160 352h160v16h-160zm0 0" />
// <path d="m160 384h160v16h-160zm0 0" />
// <path d="m272 320h48v16h-48zm0 0" />
// </svg>
// );
// }

@ -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'

@ -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();
}
}

@ -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<IToasterComponentProps> {
constructor(props: IToasterComponentProps) {
super(props);
props.toaster.updateComponent = () => this.forceUpdate();
}
render() {
const { currentSelected } = this.props.toaster;
return (
<Wrapper>
{currentSelected.length
? currentSelected.map((item, index) => {
return (
<ToasterItem
background={"backgroundToaster"}
pad={"xsmall"}
index={index}
style={{
borderRadius: "6px",
marginBottom: `${index * 70 + 10}px`,
}}
>
<Text color={"headerText"}>
{typeof item.message === "function"
? item.message()
: item.message}
</Text>
</ToasterItem>
);
})
: null}
</Wrapper>
);
}
}

@ -0,0 +1,2 @@
export * from './Toaster'
export * from './ToasterComponent'

@ -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<number> {
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;
}

@ -0,0 +1,7 @@
export const binanceAddressMap: any = {
"0x71b413da5cc729ff805e2dca1dcde04d29ef2b6a": "Binance Gateway",
"0x1548c6227cbd78e51eb0a679c1f329b9a5a99beb": "DaVinci Images",
"0xbde650853b535d738ce67f1bdeb335e38834a9e9": "DaVinci Music",
"0x474d8fd12780fbe2b7b7bd74eb326bb75ded91d8": "DaVinci Videos",
"0x51f6290510be3c802471e27f0843a3a54a8226df": "DaVinci Books",
};

@ -0,0 +1 @@
export const config = {}

File diff suppressed because one or more lines are too long

@ -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<ERC1155_Pool>(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<string, ERC1155>;

@ -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<ERC20_Pool>(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<string, Erc20>;

@ -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<ERC721_Pool>(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<string, ERC721>;

@ -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<currencyType>(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";

@ -0,0 +1,75 @@
import { useState, useEffect, useRef, Dispatch } from 'react'
export type APIPollingOptions<DataType> = {
fetchFunc: () => Promise<DataType>
initialState: DataType
delay: number
onError?: (e: Error, setData?: Dispatch<any>) => void
updateTrigger?: any
}
function useAPIPolling<DataType>(opts: APIPollingOptions<DataType>): DataType {
const { initialState, fetchFunc, delay, onError, updateTrigger } = opts
const timerId = useRef<any>()
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

@ -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<themeType>(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";

@ -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<any>({})
const options: APIPollingOptions<any> = {
fetchFunc,
initialState: {},
delay: 30000
}
const res = useAPIPolling(options)
useEffect(() => {
setData(res)
}, [res])
return data
})

@ -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;
}

@ -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(
<React.StrictMode>
<App />
</React.StrictMode>,
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()

@ -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 (
<Box>
{items.sort(sortByOrder).map((i) => (
//@ts-ignore
<DetailItem key={i} name={i} data={data} type={type} />
))}
</Box>
);
}
export const Item = (props: { label: any; value: any }) => {
return (
<Box
direction="row"
align={"center"}
margin={{ bottom: "small" }}
pad={{ bottom: "small" }}
border={{ size: "xsmall", side: "bottom", color: "border" }}
>
<Text
style={{ width: "20%" }}
color="minorText"
size="small"
margin={{ right: "xsmall" }}
>
{props.label}
</Text>
<Text style={{ width: "80%", wordBreak: "break-all" }} size="small">
{props.value}
</Text>
</Box>
);
};
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 (
<Item
label={addressPropertyDisplayNames[name](data, { type })}
value={addressPropertyDisplayValues[name](data[name], data, { type })}
/>
);
}
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 (
<>
<Address address={value} displayHash />
{binanceAddressMap[value] ? ` (${binanceAddressMap[value]})` : null}
</>
);
},
value: (value) => <TokenValue value={value} />,
creatorAddress: (value) => <Address address={value} />,
// solidityVersion: (value) => value,
IPFSHash: (value) => value,
meta: (value) => value,
// bytecode: (value) => <ExpandString value={value || ""} />,
balance: (value) => (
<Box width={"550px"}>
<ONEValueDropdown value={value} />
</Box>
),
token: (value) => <TokensInfo value={value} />,
name: (value) => value,
symbol: (value) => value,
decimals: (value) => value,
totalSupply: (value, data) => (
<Box direction={"row"}>
<TokenValue
value={value}
tokenAddress={data.address}
hideSymbol
formatNumber
/>
<Tip
dropProps={{ align: { left: "right" } }}
content={
<TipContent
message={`last update block height ${formatNumber(
+data.lastUpdateBlockNumber
)}`}
/>
}
plain
>
<span style={{ marginLeft: "5px" }}>
<CircleQuestion size="small" />
</span>
</Tip>
</Box>
),
holders: (value: string, data: any) => {
return (
<Box direction={"row"}>
<>{formatNumber(+value)}</>
<Tip
dropProps={{ align: { left: "right" } }}
content={
<TipContent
message={`last update block height ${formatNumber(
+data.lastUpdateBlockNumber
)}`}
/>
}
plain
>
<span style={{ marginLeft: "5px" }}>
<CircleQuestion size="small" />
</span>
</Tip>
</Box>
);
},
description: (value) => <>{value}</>,
transactionHash: (value) => <Address address={value} type={"tx"} />,
circulating_supply: (value, data) => (
<Box direction={"row"}>
<TokenValue
value={value}
tokenAddress={data.address}
hideSymbol
formatNumber
/>
<Tip
dropProps={{ align: { left: "right" } }}
content={
<TipContent
message={`last update block height ${formatNumber(
+data.lastUpdateBlockNumber
)}`}
/>
}
plain
>
<span style={{ marginLeft: "5px" }}>
<CircleQuestion size="small" />
</span>
</Tip>
</Box>
),
};
function sortByOrder(a: string, b: string) {
return addressPropertyOrder[a] - addressPropertyOrder[b];
}
const addressPropertyOrder: Record<string, number> = {
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";
}

@ -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<string[]>(
[...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 (
<ViewWrapper direction="column" margin={{ bottom: "medium" }}>
<NameWrapper>
<Text size="small">
{index + 1}. {abiMethod.name}
</Text>
</NameWrapper>
<Box pad="20px">
{abiMethod.stateMutability === "payable" ? (
<Field gap="5px">
<Text size="small">
payableAmount <span>ONE</span>
</Text>
<SmallTextInput
value={amount}
placeholder={`payableAmount (ONE)`}
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setAmount(evt.currentTarget.value)
}
/>
</Field>
) : null}
{abiMethod.inputs && abiMethod.inputs.length ? (
<Box gap="12px">
{abiMethod.inputs.map((input, idx) => {
const name = input.name || "<input>";
return (
<Field gap="5px">
<Text size="small">
{name} <span>({input.type})</span>
</Text>
<SmallTextInput
value={inputsValue[idx]}
placeholder={`${name} (${input.type})`}
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setInputValue(evt.currentTarget.value, idx)
}
/>
</Field>
);
})}
</Box>
) : null}
{!result || abiMethod.inputs?.length ? (
<Box width="100px" margin={{ top: "20px", bottom: "18px" }}>
{loading ? (
<Spinner />
) : abiMethod.stateMutability === "view" ? (
<ActionButton onClick={query}>Query</ActionButton>
) : (
<ActionButton disabled={!props.metamaskAddress} onClick={query}>
Write
</ActionButton>
)}
</Box>
) : null}
{abiMethod.outputs
? abiMethod.outputs.map((input) => {
return (
<Box>
{result ? (
<Text size="small">
{result} <GreySpan>{input.type}</GreySpan>
</Text>
) : (
<Text size="small">
{"-> "}
{input.type}
</Text>
)}
</Box>
);
})
: null}
{error && (
<Text color="red" size="small" style={{ marginTop: 5 }}>
{error}
</Text>
)}
</Box>
</ViewWrapper>
);
};

@ -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 (
<Box margin={{ bottom: "medium" }}>
{metamaskAddress ? (
<Text size="small">User address: {metamaskAddress}</Text>
) : (
<Box width="200px">
<ActionButton onClick={signInMetamask}>Sign in Metamask</ActionButton>
</Box>
)}
</Box>
);
};

@ -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;
}
});
};

@ -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 (
<VerifiedContractDetails
sourceCode={props.sourceCode}
contracts={props.contracts}
address={props.address}
/>
);
}
if (!!props.contracts) {
return <NoVerifiedContractDetails contracts={props.contracts} />;
}
return null;
};
export const AbiMethods = (props: {
address: string;
abi: AbiItem[];
metamaskAddress?: string;
}) => {
return (
<Box>
{props.abi.map((abiMethod, idx) =>
abiMethod.name ? (
<AbiMethodsView
abiMethod={abiMethod}
address={props.address}
index={idx}
metamaskAddress={props.metamaskAddress}
/>
) : null
)}
</Box>
);
};
export const NoVerifiedContractDetails = (props: {
contracts: AddressDetails;
}) => {
const history = useHistory();
return (
<Box style={{ padding: "10px" }} margin={{ top: "medium" }}>
<Box direction="column" gap="30px">
<Box direction="row" gap="5px">
Are you the contract creator?
<Text
size="small"
style={{ cursor: "pointer" }}
onClick={() => history.push(`/verifycontract`)}
color="brand"
>
Verify and Publish
</Text>{" "}
your contract source code today!
</Box>
<Box direction="column">
<Item
label="Solidity version"
value={props.contracts.solidityVersion}
/>
{props.contracts.IPFSHash ? (
<Item label="IPFS hash" value={props.contracts.IPFSHash} />
) : null}
<Item
label="Bytecode"
value={
<StyledTextArea readOnly={true} rows={15} cols={100}>
{props.contracts.bytecode || ""}
</StyledTextArea>
}
/>
</Box>
</Box>
</Box>
);
};
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 (
<TabBox
onClick={props.onClick}
style={{ background: props.selected ? "#77838f" : "transparent" }}
>
<Text size="small" color={props.selected ? "white" : "black"}>
{props.text}
</Text>
</TabBox>
);
};
export const VerifiedContractDetails = (props: {
sourceCode: ISourceCode;
address: string;
contracts?: AddressDetails | null;
}) => {
const [tab, setTab] = useState<V_TABS>(V_TABS.CODE);
const [metamaskAddress, setMetamask] = useState("");
return (
<Box direction="column">
<Box direction="row" align="center" margin={{ top: "medium" }}>
<TabButton
text={V_TABS.CODE}
onClick={() => setTab(V_TABS.CODE)}
selected={tab === V_TABS.CODE}
/>
{props.sourceCode.abi ? (
<>
<TabButton
text={V_TABS.READ}
onClick={() => setTab(V_TABS.READ)}
selected={tab === V_TABS.READ}
/>
<TabButton
text={V_TABS.WRITE}
onClick={() => setTab(V_TABS.WRITE)}
selected={tab === V_TABS.WRITE}
/>
</>
) : null}
</Box>
{tab === V_TABS.CODE ? (
<Box style={{ padding: "10px" }} margin={{ top: "medium" }}>
<Box direction="column" gap="30px">
<Box direction="column">
<Item
label="Contract Name"
value={props.sourceCode.contractName}
/>
<Item
label="Compiler Version"
value={props.sourceCode.compiler}
/>
<Item
label="Optimization Enabled"
value={
props.sourceCode.optimizer ||
"No" +
(Number(props.sourceCode.optimizerTimes)
? ` with ${props.sourceCode.optimizerTimes} runs`
: "")
}
/>
<Item
label="Contract Source Code Verified"
value={
<StyledTextArea readOnly={true} rows={15} cols={100}>
{props.sourceCode.sourceCode || ""}
</StyledTextArea>
}
/>
{props.contracts ? (
<Item
label="Bytecode"
value={
<StyledTextArea readOnly={true} rows={7} cols={100}>
{props.contracts.bytecode || ""}
</StyledTextArea>
}
/>
) : null}
</Box>
</Box>
</Box>
) : null}
{tab === V_TABS.WRITE && props.sourceCode.abi ? (
<Box style={{ padding: "10px" }} margin={{ top: "medium" }}>
<Wallet onSetMetamask={setMetamask} />
<AbiMethods
abi={props.sourceCode.abi.filter(
(a) =>
a.stateMutability !== "view" &&
!!a.name &&
a.type === "function"
)}
address={props.address}
metamaskAddress={metamaskAddress}
/>
</Box>
) : null}
{tab === V_TABS.READ && props.sourceCode.abi ? (
<Box style={{ padding: "10px" }} margin={{ top: "medium" }}>
<AbiMethods
abi={props.sourceCode.abi.filter(
(a) => a.stateMutability === "view" && a.type === "function"
)}
address={props.address}
/>
</Box>
) : null}
</Box>
);
};

@ -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 <span></span>;
}
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 (
<Box>
<Box style={{ width: "550px" }}>
<Dropdown<Token>
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 (
<Box
direction="column"
style={{
width: "100%",
flex: "0 0 auto",
justifyContent: "space-between",
marginBottom: "10px",
padding: "5px",
}}
>
<Box direction={"row"}>
<Box style={{ flex: "1 1 50%" }} direction={'row'}>
<Address
address={item.tokenAddress}
style={{ flex: "1 1 50%" }}
/>
<Text
size={"small"}
color={"minorText"}
style={{ marginLeft: "5px" }}
>
({symbol})
</Text>
</Box>
<TokenValueBalanced
value={item.balance}
tokenAddress={item.tokenAddress}
style={{ flex: "1 1 50%", wordBreak: "break-word" }}
/>
{item.isERC1155 && (item as any).needUpdate ? (
<Tip
dropProps={{ align: { left: "right" } }}
content={<TipContent message={"Outdated"} />}
plain
>
<span>
<Alert size="small" />
</span>
</Tip>
) : null}
</Box>
{item.isERC1155 ? (
<Text size={"small"} color={"minorText"}>
Token ID: {item.tokenID}{" "}
</Text>
) : null}
</Box>
);
}}
renderValue={() => (
<Box direction={"row"} style={{ paddingTop: "3px" }}>
{erc20Tokens.length ? (
<Box style={{ marginRight: "10px" }} direction={"row"}>
HRC20{" "}
<Box
background={"backgroundBack"}
style={{
width: "20px",
height: "20px",
marginLeft: "5px",
textAlign: "center",
borderRadius: "4px",
}}
>
{erc20Tokens.length}
</Box>
</Box>
) : null}
{erc721Tokens.length ? (
<Box direction={"row"}>
HRC721{" "}
<Box
background={"backgroundBack"}
style={{
width: "20px",
height: "20px",
marginLeft: "5px",
textAlign: "center",
borderRadius: "4px",
}}
>
{erc721Tokens.length}
</Box>
</Box>
) : null}
{erc1155Tokens.length ? (
<Box direction={"row"}>
HRC1155{" "}
<Box
background={"backgroundBack"}
style={{
width: "20px",
height: "20px",
marginLeft: "5px",
textAlign: "center",
borderRadius: "4px",
}}
>
{erc1155Tokens.length}
</Box>
</Box>
) : null}
</Box>
)}
group={[
{
groupBy: "isERC20",
renderGroupItem: () => (
<Box
style={{
minHeight: "35px",
borderRadius: "8px",
marginBottom: "10px",
marginTop: "10px",
}}
pad={"xsmall"}
background={"backgroundBack"}
>
<Text>HRC20 tokens</Text>
</Box>
),
},
{
groupBy: "isERC721",
renderGroupItem: () => (
<Box
style={{
minHeight: "35px",
borderRadius: "8px",
marginBottom: "10px",
marginTop: "10px",
}}
pad={"xsmall"}
background={"backgroundBack"}
>
<Text>HRC721 tokens</Text>
</Box>
),
},
{
groupBy: "isERC1155",
renderGroupItem: () => (
<Box
style={{
minHeight: "35px",
borderRadius: "8px",
marginBottom: "10px",
marginTop: "10px",
}}
pad={"xsmall"}
background={"backgroundBack"}
>
<Text>HRC1155 tokens</Text>
</Box>
),
},
]}
/>
</Box>
</Box>
);
}
function TokenInfo(props: { value: Token }) {
const { value } = props;
return (
<Box
direction="row"
style={{ minWidth: "500px", maxWidth: "550px", flex: "0 0 auto" }}
margin={{ bottom: "3px" }}
gap="medium"
>
<Address address={value.tokenAddress} style={{ flex: "1 1 50%" }} />
<TokenValue
value={value.balance}
tokenAddress={value.tokenAddress}
style={{ flex: "1 1 50%", wordBreak: "break-word" }}
/>
</Box>
);
}
function filterWithBalance(balance: string) {
return !!+balance;
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save