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. 59
      src/api/explorerV1.ts
  5. 1
      src/pages/AddressPage/AddressDetails.tsx
  6. 167
      src/pages/AddressPage/ContractDetails/index.tsx
  7. 51
      src/pages/AddressPage/tabs/holders/HoldersTab.tsx
  8. 71
      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.\ ## Dev Environment
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\ ### First Install
You will also see any lint errors in the console.
### `yarn test` 1) Clone repo:
Launches the test runner in the interactive watch mode.\ ```bash
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 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.\ 1) Create a new file: `.env.local`
It correctly bundles React in production mode and optimizes the build for the best performance. 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.\ ### Build
Your app is ready to be deployed! To create production build run command:
```
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. yarn build
```
### `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/).

44253
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -14,6 +14,8 @@ export interface IVerifyContractData {
isLoading: boolean; isLoading: boolean;
argsLoading: boolean; argsLoading: boolean;
error: string; error: string;
tab: string;
fileList?: File[];
language: number; language: number;
} }
@ -27,9 +29,61 @@ export interface IVerifyContractDataSendData {
constructorArguments: string; constructorArguments: string;
chainType: string; chainType: string;
contractName: string; contractName: string;
fileList?: File[],
tab: string,
} }
export const verifyContractCode = async (data: IVerifyContractDataSendData) => { export const verifyContractCode = async (data: IVerifyContractDataSendData) => {
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 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,
}
);
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( const response = await fetch(
`${process.env.REACT_APP_EXPLORER_V1_API_URL}codeVerification`, `${process.env.REACT_APP_EXPLORER_V1_API_URL}codeVerification`,
{ {
@ -53,6 +107,7 @@ export const verifyContractCode = async (data: IVerifyContractDataSendData) => {
} }
return body; return body;
}
}; };
export const loadSourceCode = async (address: string): Promise<ISourceCode> => { export const loadSourceCode = async (address: string): Promise<ISourceCode> => {
@ -85,9 +140,13 @@ export interface ISourceCode {
optimizer: string; optimizer: string;
optimizerTimes: string; optimizerTimes: string;
sourceCode: string; sourceCode: string;
supporting: any;
libraries: string[]; libraries: string[];
constructorArguments: string; constructorArguments: string;
chainType: string; chainType: string;
contractName: string; contractName: string;
abi?: AbiItem[]; abi?: AbiItem[];
proxyAddress?: string;
proxyDetails?: any;
proxy?: any;
} }

@ -20,7 +20,6 @@ import styled from "styled-components";
export const StyledBox = styled(Box)` export const StyledBox = styled(Box)`
transition: all 0.2s linear; transition: all 0.2s linear;
border-radius: 2px; border-radius: 2px;
padding-left: 5px;
`; `;
const AddressDescription = styled(Box)` 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: { export const NoVerifiedContractDetails = (props: {
contracts: AddressDetails; contracts: AddressDetails;
address: string; address: string;
@ -119,6 +199,8 @@ enum V_TABS {
CODE = "Code", CODE = "Code",
READ = "Read Contract", READ = "Read Contract",
WRITE = "Write 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 }>`
@ -166,9 +248,12 @@ export const VerifiedContractDetails = (props: {
abiString = JSON.stringify(props.sourceCode.abi, null, 4); abiString = JSON.stringify(props.sourceCode.abi, null, 4);
} catch { } } catch { }
console.log(props.sourceCode);
return ( return (
<Box direction="column"> <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 <TabButton
text={V_TABS.CODE} text={V_TABS.CODE}
onClick={() => setTab(V_TABS.CODE)} onClick={() => setTab(V_TABS.CODE)}
@ -188,6 +273,21 @@ export const VerifiedContractDetails = (props: {
/> />
</> </>
) : null} ) : 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> </Box>
{tab === V_TABS.CODE ? ( {tab === V_TABS.CODE ? (
<Box style={{ padding: "10px" }} margin={{ top: "medium" }}> <Box style={{ padding: "10px" }} margin={{ top: "medium" }}>
@ -211,14 +311,27 @@ export const VerifiedContractDetails = (props: {
: "") : "")
} }
/> />
{props.sourceCode.sourceCode && (!props.sourceCode.supporting || props.sourceCode.supporting.length === 0) &&
<Item <Item
label="Contract Source Code Verified" label="Contract Source Code Verified"
value={ value={
<StyledTextArea readOnly={true} rows={15} cols={100}> <StyledTextArea readOnly={true} rows={15} cols={100} value={props.sourceCode.sourceCode || ""}>
{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> </StyledTextArea>
} }
/> />
})}
<Item <Item
label="ABI" label="ABI"
value={ value={
@ -251,6 +364,19 @@ export const VerifiedContractDetails = (props: {
</Box> </Box>
</Box> </Box>
) : null} ) : 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 ? ( {tab === V_TABS.WRITE && props.sourceCode.abi ? (
<Box style={{ padding: "10px" }} margin={{ top: "medium" }}> <Box style={{ padding: "10px" }} margin={{ top: "medium" }}>
<Wallet onSetMetamask={setMetamask} onSetChainId={setChainId} /> <Wallet onSetMetamask={setMetamask} onSetChainId={setChainId} />
@ -268,17 +394,40 @@ export const VerifiedContractDetails = (props: {
</Box> </Box>
) : null} ) : null}
{tab === V_TABS.READ && props.sourceCode.abi ? ( {tab === V_TABS.READ_PROXY && props.sourceCode.proxy ? (
<Box style={{ padding: "10px" }} margin={{ top: "medium" }}> <Box style={{ padding: "10px" }}>
<ProxyContractDetails address={props.sourceCode.proxyAddress || ""} proxy={props.sourceCode.proxyDetails}></ProxyContractDetails>
<AbiMethods <AbiMethods
abi={props.sourceCode.abi.filter( abi={props.sourceCode.proxy.abi.filter(
(a) => a.stateMutability === "view" && a.type === "function" (a: { stateMutability: string; type: string; }) => a.stateMutability === "view" && a.type === "function"
)} )}
address={props.address} address={props.address || ""}
isRead={V_TABS.READ === tab} 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> </Box>
) : null} ) : null}
</Box> </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 { useERC20Pool } from "src/hooks/ERC20_Pool";
import { useERC721Pool } from "src/hooks/ERC721_Pool"; import { useERC721Pool } from "src/hooks/ERC721_Pool";
import { Filter } from "src/types"; import { Filter } from "src/types";
import Big from "big.js";
const getColumns = ( const getColumns = (
id: string, id: string,
type: "erc20" | "erc721" | "erc1155" type: "erc20" | "erc721" | "erc1155"
): ColumnConfig<IHoldersInfo>[] => { ): 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", property: "ownerAddres",
header: ( header: (
@ -34,7 +48,7 @@ const getColumns = (
Balance Balance
</Text> </Text>
), ),
render: (data) => { render: (data: any) => {
return type === "erc1155" ? ( return type === "erc1155" ? (
<>{data.balance}</> <>{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: { export function HoldersTab(props: {
@ -59,6 +91,11 @@ export function HoldersTab(props: {
erc721Map[props.id]?.holders || erc721Map[props.id]?.holders ||
erc1155Map[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 limitValue = localStorage.getItem("tableLimitValue");
const initFilter: Partial<Filter> = { 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); setHolders(holdersData);
} catch (err) { } catch (err) {
setHolders([]); setHolders([]);

@ -1,5 +1,6 @@
import { import {
Box, Box,
FileInput,
Heading, Heading,
Select, Select,
Spinner, Spinner,
@ -30,11 +31,39 @@ const Wrapper = styled(Box)`
export function uniqid(prefix = "", random = false) { export function uniqid(prefix = "", random = false) {
const sec = Date.now() * 1000 + Math.random() * 1000; const sec = Date.now() * 1000 + Math.random() * 1000;
const id = sec.toString(16).replace(/\./g, "").padEnd(14, "0"); const id = sec.toString(16).replace(/\./g, "").padEnd(14, "0");
return `${prefix}${id}${ return `${prefix}${id}${random ? `.${Math.trunc(Math.random() * 100000000)}` : ""
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< class VerifyContractBase extends React.Component<
{ {
isLessTablet: boolean; isLessTablet: boolean;
@ -56,6 +85,7 @@ class VerifyContractBase extends React.Component<
argsLoading: false, argsLoading: false,
statusText: "", statusText: "",
error: "", error: "",
tab: V_TABS.SINGLE,
language: 0 language: 0
}; };
@ -237,8 +267,20 @@ class VerifyContractBase extends React.Component<
</Box> </Box>
</Field> </Field>
</Box> </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> <Text>Enter the Solidity Contract Code below</Text>
<TextArea <TextArea
style={{ minHeight: "300px" }} style={{ minHeight: "300px" }}
@ -250,7 +292,30 @@ class VerifyContractBase extends React.Component<
}} }}
disabled={isLoading} disabled={isLoading}
/> />
</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>
}
<Field margin={"small"}> <Field margin={"small"}>
<Box direction="row" justify="between"> <Box direction="row" justify="between">

Loading…
Cancel
Save