Merge branch 'master' into feature/address_full_history

pull/148/head
artemkolodko 3 years ago
commit 3ff114fb76
  1. 21
      CONTRIBUTING.md
  2. 70
      README.md
  3. 44253
      package-lock.json
  4. 95
      src/api/explorerV1.ts
  5. 1
      src/pages/AddressPage/AddressDetails.tsx
  6. 189
      src/pages/AddressPage/ContractDetails/index.tsx
  7. 51
      src/pages/AddressPage/tabs/holders/HoldersTab.tsx
  8. 75
      src/pages/VerifyContract/VerifyContract.tsx

@ -0,0 +1,21 @@
## Contributing
When contributing to this repository, please first discuss the change you wish to make via issue, discord server or any other method with the owners of this repository before making a change.
### Pull Request Process
1) Fork a [repository](https://github.com/harmony-one/explorer-v2-frontend) and create your branch from `master`;
2) Run `yarn build` in the repository root
3) Update .env.example if new environment variables is created;
4) Create pull request linked with existed issue.
### Branch Organization
Submit all changes directly to [master branch](https://github.com/harmony-one/explorer-v2-frontend/tree/master).
We don’t use separate branches for development or for upcoming releases.
### Bugs
We are using [Github Issues](https://github.com/harmony-one/explorer-v2-frontend/issues) for tracking bugs.
Before creating a new issue please check that problem doesn't already exist.
### Proposing a Change
If you have a suggestion on how to improve functionality or create a new feature you can [fill a new issue](https://github.com/harmony-one/explorer-v2-frontend/issues/new).

@ -1,48 +1,44 @@
# Getting Started with Create React App
# Harmony Explorer frontend
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Requirements
## Available Scripts
### Node.js 16.13.2 LTS
Download and install Node.js from the official website: [https://nodejs.org/](https://nodejs.org/)
In the project directory, you can run:
### yarn 1.22.17
### `yarn start`
```
npm install --global yarn
```
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
## Dev Environment
The page will reload if you make edits.\
You will also see any lint errors in the console.
### First Install
### `yarn test`
1) Clone repo:
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.
```bash
git clone https://github.com/harmony-one/explorer-v2-frontend.git
```
2) Install dependencies:
```bash
cd explorer-v2-frontend
yarn install
```
3) Run project:
```bash
yarn start
```
4) Open app page http://localhost:3000/
### `yarn build`
### Configuring app
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
1) Create a new file: `.env.local`
2) Copy env variables from `.env.example` to newly created file `.env.local`
3) Setup custom env variables values and restart the app
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/).
### Build
To create production build run command:
```
yarn build
```

44253
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -14,6 +14,8 @@ export interface IVerifyContractData {
isLoading: boolean;
argsLoading: boolean;
error: string;
tab: string;
fileList?: File[];
language: number;
}
@ -27,32 +29,85 @@ export interface IVerifyContractDataSendData {
constructorArguments: string;
chainType: string;
contractName: string;
fileList?: File[],
tab: 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),
if (data.tab === "Multiple Source Files") {
console.log("Handling multiple files");
const formData = new FormData();
data.fileList?.forEach(file=>{
formData.append(file.name, file);
});
for (const [k, v] of Object.entries(data)) {
if (k === "fileList") {
continue;
}
if (k === "language" && +v === 0) {
continue;
}
if (k === "libraries") {
formData.append(k, v.join(","));
}
else {
formData.append(k, v);
}
}
);
const body = await response.json();
const response = await fetch(
`${process.env.REACT_APP_EXPLORER_V1_API_URL}codeVerification`,
{
method: "POST",
mode: "cors",
cache: "no-cache",
credentials: "same-origin",
redirect: "follow",
referrerPolicy: "no-referrer",
body: formData,
}
);
if (response.status !== 200) {
throw new Error(body?.message);
const body = await response.json();
console.log("Handling multiple files", body);
if (response.status !== 200) {
throw new Error(body?.message);
}
return body;
}
else {
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 body;
const body = await response.json();
if (response.status !== 200) {
throw new Error(body?.message);
}
return body;
}
};
export const loadSourceCode = async (address: string): Promise<ISourceCode> => {
@ -85,9 +140,13 @@ export interface ISourceCode {
optimizer: string;
optimizerTimes: string;
sourceCode: string;
supporting: any;
libraries: string[];
constructorArguments: string;
chainType: string;
contractName: string;
abi?: AbiItem[];
proxyAddress?: string;
proxyDetails?: any;
proxy?: any;
}

@ -20,7 +20,6 @@ import styled from "styled-components";
export const StyledBox = styled(Box)`
transition: all 0.2s linear;
border-radius: 2px;
padding-left: 5px;
`;
const AddressDescription = styled(Box)`

@ -69,6 +69,86 @@ export const AbiMethods = (props: {
);
};
export const VerifiedButMissingImplementation = (props: {
address: string;
}) => {
const history = useHistory();
return (
<Box style={{ padding: "10px" }}>
<Box direction="column" gap="30px">
<Box direction="row" gap="5px">
Proxy Implementation contract
<Text
size="small"
style={{ cursor: "pointer" }}
onClick={() =>
history.push(`/address/${props.address}`)
}
color="brand"
>
{props.address}
</Text>
is not verified. Are you the contract creator?
<Text
size="small"
style={{ cursor: "pointer" }}
onClick={() =>
history.push(`/verifycontract?address=${props.address}`)
}
color="brand"
>
Verify and Publish
</Text>
your contract source code today!
</Box>
</Box>
</Box>
);
};
export const ProxyContractDetails = (props: {
address: string;
proxy: any;
}) => {
const history = useHistory();
return (
<Box style={{ padding: "10px" }}>
<Box direction="column">
<Box direction="row" gap="5px">
ABI for the implementation contract at
<Text
size="small"
style={{ cursor: "pointer" }}
onClick={() =>
history.push(`/address/${props.address}?activeTab=7`)
}
color="brand"
>
{props.address}
</Text>
</Box>
{
props.proxy?.isBeacon && <Box direction="row" gap="5px">
Upgradeable Beacon contract at
<Text
size="small"
style={{ cursor: "pointer" }}
onClick={() =>
history.push(`/address/${props?.proxy.beaconAddress}?activeTab=7`)
}
color="brand"
>
{props?.proxy.beaconAddress}
</Text>
</Box>
}
</Box>
</Box>
);
};
export const NoVerifiedContractDetails = (props: {
contracts: AddressDetails;
address: string;
@ -119,9 +199,11 @@ enum V_TABS {
CODE = "Code",
READ = "Read Contract",
WRITE = "Write Contract",
READ_PROXY = "Read as Proxy",
WRITE_PROXY = "Write as Proxy",
}
const TabBox = styled(Box)<{ selected: boolean }>`
const TabBox = styled(Box) <{ selected: boolean }>`
border: 1px solid ${(props) => props.theme.global.colors.border};
background: ${(props) =>
props.selected ? props.theme.global.colors.backgroundBack : "transparent"};
@ -164,11 +246,14 @@ export const VerifiedContractDetails = (props: {
try {
abiString = JSON.stringify(props.sourceCode.abi, null, 4);
} catch {}
} catch { }
console.log(props.sourceCode);
return (
<Box direction="column">
<Box direction="row" align="center" margin={{ top: "medium" }}>
{props.sourceCode?.proxyAddress && !props.sourceCode?.proxy && <VerifiedButMissingImplementation address={props.sourceCode.proxyAddress}/>}
<Box direction="row" align="center" margin={{ top: props.sourceCode?.proxyAddress && !props.sourceCode?.proxy ? "" : "medium" }}>
<TabButton
text={V_TABS.CODE}
onClick={() => setTab(V_TABS.CODE)}
@ -188,6 +273,21 @@ export const VerifiedContractDetails = (props: {
/>
</>
) : null}
{props.sourceCode.proxyAddress && props.sourceCode.proxy ? (
<>
<TabButton
text={V_TABS.READ_PROXY + "(new)"}
onClick={() => setTab(V_TABS.READ_PROXY)}
selected={tab === V_TABS.READ_PROXY}
/>
<TabButton
text={V_TABS.WRITE_PROXY + "(new)"}
onClick={() => setTab(V_TABS.WRITE_PROXY)}
selected={tab === V_TABS.WRITE_PROXY}
/>
</>
) : null}
</Box>
{tab === V_TABS.CODE ? (
<Box style={{ padding: "10px" }} margin={{ top: "medium" }}>
@ -206,19 +306,32 @@ export const VerifiedContractDetails = (props: {
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>
(Number(props.sourceCode.optimizerTimes)
? ` with ${props.sourceCode.optimizerTimes} runs`
: "")
}
/>
{props.sourceCode.sourceCode && (!props.sourceCode.supporting || props.sourceCode.supporting.length === 0) &&
<Item
label="Contract Source Code Verified"
value={
<StyledTextArea readOnly={true} rows={15} cols={100} value={props.sourceCode.sourceCode || ""}>
</StyledTextArea>
}
/>}
{props.sourceCode.supporting?.sources
&&
Object.keys(props.sourceCode.supporting?.sources).map((source: string, i: number) => {
return <Item
key={i}
label={`Verified ${source}`}
value={
<StyledTextArea readOnly={true} rows={15} cols={100} value={props.sourceCode.supporting.sources[source].source || ""}>
</StyledTextArea>
}
/>
})}
<Item
label="ABI"
value={
@ -251,6 +364,19 @@ export const VerifiedContractDetails = (props: {
</Box>
</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}
isRead={V_TABS.READ === tab}
/>
</Box>
) : null}
{tab === V_TABS.WRITE && props.sourceCode.abi ? (
<Box style={{ padding: "10px" }} margin={{ top: "medium" }}>
<Wallet onSetMetamask={setMetamask} onSetChainId={setChainId} />
@ -268,17 +394,40 @@ export const VerifiedContractDetails = (props: {
</Box>
) : null}
{tab === V_TABS.READ && props.sourceCode.abi ? (
<Box style={{ padding: "10px" }} margin={{ top: "medium" }}>
{tab === V_TABS.READ_PROXY && props.sourceCode.proxy ? (
<Box style={{ padding: "10px" }}>
<ProxyContractDetails address={props.sourceCode.proxyAddress || ""} proxy={props.sourceCode.proxyDetails}></ProxyContractDetails>
<AbiMethods
abi={props.sourceCode.abi.filter(
(a) => a.stateMutability === "view" && a.type === "function"
abi={props.sourceCode.proxy.abi.filter(
(a: { stateMutability: string; type: string; }) => a.stateMutability === "view" && a.type === "function"
)}
address={props.address}
isRead={V_TABS.READ === tab}
address={props.address || ""}
isRead={V_TABS.READ_PROXY === tab}
/>
</Box>
) : null}
{tab === V_TABS.WRITE_PROXY && props.sourceCode.proxy ? (
<Box style={{ padding: "10px" }}>
<Wallet onSetMetamask={setMetamask} onSetChainId={setChainId} />
<ProxyContractDetails address={props.sourceCode.proxyAddress || ""} proxy={props.sourceCode.proxyDetails}></ProxyContractDetails>
<AbiMethods
abi={props.sourceCode.proxy.abi.filter(
(a: { stateMutability: string; name: any; type: string; }) =>
a.stateMutability !== "view" &&
!!a.name &&
a.type === "function"
)}
address={props.address || ""}
metamaskAddress={metamaskAddress}
validChainId={validChainId}
/>
</Box>
) : null}
</Box>
);
};
// 1. get implementation address of the proxy
// 2. look up when they TAB to the READ or WRITE PROXY. get the source code (fetchContractSource)
// 3. supply as props and enable the read/write proxy code ()

@ -8,12 +8,26 @@ import { useERC1155Pool } from "src/hooks/ERC1155_Pool";
import { useERC20Pool } from "src/hooks/ERC20_Pool";
import { useERC721Pool } from "src/hooks/ERC721_Pool";
import { Filter } from "src/types";
import Big from "big.js";
const getColumns = (
id: string,
type: "erc20" | "erc721" | "erc1155"
): ColumnConfig<IHoldersInfo>[] => {
return [
const columns = [
{
property: "rank",
header: (
<Text
color="minorText"
size="small"
style={{ fontWeight: 300, width: "30px" }}
>
Rank
</Text>
),
render: (data: any) => <div>{data.rank}</div>,
},
{
property: "ownerAddres",
header: (
@ -34,7 +48,7 @@ const getColumns = (
Balance
</Text>
),
render: (data) => {
render: (data: any) => {
return type === "erc1155" ? (
<>{data.balance}</>
) : (
@ -43,6 +57,24 @@ const getColumns = (
},
},
];
if (type === 'erc20' || type === 'erc721') {
columns.push( {
property: "percentage",
header: (
<Text
color="minorText"
size="small"
style={{ fontWeight: 300 }}
>
Percentage
</Text>
),
render: (data: any) => <div>{data.percentage}</div>,
})
}
return columns
};
export function HoldersTab(props: {
@ -59,6 +91,11 @@ export function HoldersTab(props: {
erc721Map[props.id]?.holders ||
erc1155Map[props.id]?.holders;
const totalSupply =
erc20Map[props.id]?.totalSupply ||
erc721Map[props.id]?.totalSupply ||
erc1155Map[props.id]?.totalSupply;
const limitValue = localStorage.getItem("tableLimitValue");
const initFilter: Partial<Filter> = {
@ -117,6 +154,16 @@ export function HoldersTab(props: {
}
}
holdersData = holdersData.map((item, index) => {
return {
...item,
rank: filter.offset + index + 1,
percentage: totalSupply && +totalSupply > 0
? Big(item.balance).div(Big(totalSupply)).mul(100).toFixed(4) + '%'
: ''
}
})
setHolders(holdersData);
} catch (err) {
setHolders([]);

@ -1,5 +1,6 @@
import {
Box,
FileInput,
Heading,
Select,
Spinner,
@ -30,11 +31,39 @@ const Wrapper = styled(Box)`
export function uniqid(prefix = "", random = false) {
const sec = Date.now() * 1000 + Math.random() * 1000;
const id = sec.toString(16).replace(/\./g, "").padEnd(14, "0");
return `${prefix}${id}${
random ? `.${Math.trunc(Math.random() * 100000000)}` : ""
}`;
return `${prefix}${id}${random ? `.${Math.trunc(Math.random() * 100000000)}` : ""
}`;
}
enum V_TABS {
SINGLE = "Single Source File",
MULTI = "Multiple Source Files",
}
const TabBox = styled(Box) <{ selected: boolean }>`
border: 1px solid ${(props) => props.theme.global.colors.border};
background: ${(props) =>
props.selected ? props.theme.global.colors.backgroundBack : "transparent"};
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} selected={props.selected}>
<Text size="small" color={"minorText"}>
{props.text}
</Text>
</TabBox>
);
};
class VerifyContractBase extends React.Component<
{
isLessTablet: boolean;
@ -56,6 +85,7 @@ class VerifyContractBase extends React.Component<
argsLoading: false,
statusText: "",
error: "",
tab: V_TABS.SINGLE,
language: 0
};
@ -237,8 +267,20 @@ class VerifyContractBase extends React.Component<
</Box>
</Field>
</Box>
<Box direction="row" align="center" margin={{ top: "medium" }}>
<TabButton
text={V_TABS.SINGLE}
onClick={() => this.setState({ tab: V_TABS.SINGLE })}
selected={this.state.tab === V_TABS.SINGLE}
/>
<TabButton
text={V_TABS.MULTI}
onClick={() => this.setState({ tab: V_TABS.MULTI })}
selected={this.state.tab === V_TABS.MULTI}
/>
<Field margin={"small"}>
</Box>
{this.state.tab === V_TABS.SINGLE && <Field margin={"small"}>
<Text>Enter the Solidity Contract Code below</Text>
<TextArea
style={{ minHeight: "300px" }}
@ -250,7 +292,30 @@ class VerifyContractBase extends React.Component<
}}
disabled={isLoading}
/>
</Field>
</Field>}
{
this.state.tab === V_TABS.MULTI &&
<Field margin={"small"}>
<Text>Select multiple solidity source files</Text>
<FileInput
name="file"
max="100000"
multiple
onChange={event => {
if (!event.target.files) return;
const fileList = event.target.files;
const files = [];
for (let i = 0; i < fileList?.length; i += 1) {
const file = fileList[i];
files.push(file);
}
console.log(files);
this.setState({fileList:files});
}}
/>
</Field>
}
<Field margin={"small"}>
<Box direction="row" justify="between">

Loading…
Cancel
Save