commit
5cae66ee5a
@ -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 |
@ -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 = {}; |
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&display=swap" |
||||
rel="stylesheet" |
||||
/> |
||||
<link |
||||
href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@300;400;700&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> |
After Width: | Height: | Size: 5.2 KiB |
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)); |
||||
}); |
||||
}; |
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> |
||||
{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 |
||||
<CaretUpFill size="small" /> |
||||
</> |
||||
) : ( |
||||
<> |
||||
Show more |
||||
<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 |
||||
<CaretUpFill size="small" /></> |
||||
: <>Show more |
||||
<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} /> |
||||
|
||||
{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> |
||||
), |
||||
}) |
||||
} |
||||
/> |
||||
|
||||
<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> |
||||
), |
||||
}) |
||||
} |
||||
/> |
||||
|
||||
<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> |
||||
), |
||||
}) |
||||
} |
||||
/> |
||||
|
||||
</> |
||||
)} |
||||
{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> |
||||
|
||||
{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 : |
||||
</Text> |
||||
<Address address={e.parsed["$0"].toLowerCase()} /> |
||||
|
||||
<Text size="small" color="minorText"> |
||||
To : |
||||
</Text> |
||||
<Address address={e.parsed["$1"].toLowerCase()} /> |
||||
</Box> |
||||
<Box align={"center"} direction={"row"}> |
||||
<Text size="small" color="minorText"> |
||||
Value : |
||||
</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 |
||||
<CaretUpFill size="small" /> |
||||
</> |
||||
) : ( |
||||
<> |
||||
Show more |
||||
<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} |
||||
|
||||
</> |
||||
)} |
||||
{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"> </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: ${price} </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" /> |
||||
{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…
Reference in new issue