Feature/add logs history (#113)

* Add events history for ERC20 contracts

* Add event method decode
pull/117/head
Artem 3 years ago committed by GitHub
parent d8024cf895
commit 7089ea8a26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      package.json
  2. 7
      src/api/client.ts
  3. 8
      src/components/tables/TransactionsTable.tsx
  4. 12
      src/components/transaction/helpers.tsx
  5. 2
      src/components/ui/Pagination/index.tsx
  6. 7
      src/pages/AddressPage/index.tsx
  7. 278
      src/pages/AddressPage/tabs/events/Events.tsx
  8. 5
      src/pages/AllBlocksPage/AllBlocksTable.tsx
  9. 1
      src/pages/ERC721List/ERC721Table.tsx
  10. 4
      src/pages/MainPage/LatestTransactionsTable.tsx
  11. 3
      src/pages/TransactionPage/index.tsx
  12. 5
      src/pages/VerifyContract/VerifyContract.tsx
  13. 5
      src/types/blockchain.ts
  14. 4
      src/web3/parseByteCode.tsx

@ -61,6 +61,7 @@
"prettier": "^2.2.1", "prettier": "^2.2.1",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"typescript": "^4.1.2", "typescript": "^4.1.2",
"web-vitals": "^1.0.1" "web-vitals": "^1.0.1",
"react-error-overlay": "6.0.9"
} }
} }

@ -5,6 +5,7 @@ import {
RPCStakingTransactionHarmony, RPCStakingTransactionHarmony,
RPCTransactionHarmony, RPCTransactionHarmony,
RelatedTransaction, RelatedTransaction,
Log, LogDetailed
} from "src/types"; } from "src/types";
import { import {
IHoldersInfo, IHoldersInfo,
@ -71,7 +72,11 @@ export function getInternalTransactionsByField(params: any[]) {
} }
export function getTransactionLogsByField(params: any[]) { export function getTransactionLogsByField(params: any[]) {
return transport("getLogsByField", params) as Promise<any>; return transport("getLogsByField", params) as Promise<Log[]>;
}
export function getDetailedTransactionLogsByField(params: any[]) {
return transport("getDetailedLogsByField", params) as Promise<LogDetailed[]>;
} }
export async function getByteCodeSignatureByHash(params: [string]) { export async function getByteCodeSignatureByHash(params: [string]) {

@ -157,6 +157,7 @@ interface TransactionTableProps {
primaryKey?: string; primaryKey?: string;
showPages?: boolean showPages?: boolean
textType?: string textType?: string
paginationOptions?: string[]
} }
export function TransactionsTable(props: TransactionTableProps) { export function TransactionsTable(props: TransactionTableProps) {
@ -177,7 +178,8 @@ export function TransactionsTable(props: TransactionTableProps) {
noScrollTop, noScrollTop,
minWidth = "1310px", minWidth = "1310px",
showPages = false, showPages = false,
textType = 'transaction' textType = 'transaction',
paginationOptions,
} = props; } = props;
const _IsLoading = isLoading; const _IsLoading = isLoading;
@ -238,7 +240,7 @@ export function TransactionsTable(props: TransactionTableProps) {
alwaysOpenedRowDetails={props.rowDetails ? true : false} alwaysOpenedRowDetails={props.rowDetails ? true : false}
tableProps={{ tableProps={{
className: "g-table-header", className: "g-table-header",
style: { width: "100%", minWidth }, style: { width: "100%", minWidth, tableLayout: 'auto' },
columns: columns ? columns : getColumns({ history }), columns: columns ? columns : getColumns({ history }),
data: data, data: data,
step, step,
@ -271,7 +273,7 @@ export function TransactionsTable(props: TransactionTableProps) {
align="center" align="center"
margin={{ top: "medium" }} margin={{ top: "medium" }}
> >
<PaginationRecordsPerPage filter={filter} onChange={setFilter} /> <PaginationRecordsPerPage filter={filter} options={paginationOptions} onChange={setFilter} />
<PaginationNavigator <PaginationNavigator
onChange={setFilter} onChange={setFilter}
isLoading={isLoading} isLoading={isLoading}

@ -1,12 +1,8 @@
import { Block, IHexSignature, RPCTransactionHarmony } from "../../types"; import { IHexSignature, RPCTransactionHarmony } from "../../types";
import { import {
Clone, FormNextLink
FormNextLink,
FormPreviousLink,
StatusGood
} from "grommet-icons"; } from "grommet-icons";
import React from "react"; import React from "react";
import {blockPropertyDisplayValues} from "../block/helpers";
import { import {
Address, Address,
BlockHash, BlockHash,
@ -15,14 +11,10 @@ import {
TransactionHash, TransactionHash,
ONEValue, ONEValue,
StakingTransactionTypeValue, StakingTransactionTypeValue,
CalculateFee,
formatNumber,
RelativeTimer,
DateTime DateTime
} from "../ui"; } from "../ui";
import {Box} from "grommet"; import {Box} from "grommet";
import {CopyBtn} from "../ui/CopyBtn"; import {CopyBtn} from "../ui/CopyBtn";
import styled from "styled-components";
import { TxInput } from "./TransactionInput"; import { TxInput } from "./TransactionInput";
export const todo = {}; export const todo = {};

@ -117,7 +117,7 @@ function Pagination(props: PaginationProps) {
interface ElementsPerPage { interface ElementsPerPage {
filter: Filter; filter: Filter;
onChange: (filter: Filter) => void; onChange: (filter: Filter) => void;
options?: number[]; options?: string[];
} }
const defaultOptions: string[] = ["10", "25", "50", "100"]; const defaultOptions: string[] = ["10", "25", "50", "100"];

@ -31,6 +31,7 @@ import { getAddress } from "src/utils";
import { useCurrency } from "src/hooks/ONE-ETH-SwitcherHook"; import { useCurrency } from "src/hooks/ONE-ETH-SwitcherHook";
import { HoldersTab } from "./tabs/holders/HoldersTab"; import { HoldersTab } from "./tabs/holders/HoldersTab";
import { parseHexToText } from "../../web3/parseHex"; import { parseHexToText } from "../../web3/parseHex";
import { EventsTab } from "./tabs/events/Events";
export function AddressPage() { export function AddressPage() {
const history = useHistory(); const history = useHistory();
@ -374,6 +375,12 @@ export function AddressPage() {
</Tab> </Tab>
) : null} ) : null}
{type === "erc20" &&
<Tab title={<Text size="small">Events</Text>}>
<EventsTab id={id}/>
</Tab>
}
{/*{type === "erc1155" && inventory.length ? (*/} {/*{type === "erc1155" && inventory.length ? (*/}
{/* <Tab*/} {/* <Tab*/}
{/* title={<Text size="small">Inventory ({inventory.length})</Text>}*/} {/* title={<Text size="small">Inventory ({inventory.length})</Text>}*/}

@ -0,0 +1,278 @@
import { Box, ColumnConfig, Spinner, Text, Tip } from "grommet";
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import dayjs from "dayjs";
import { TransactionsTable } from "src/components/tables/TransactionsTable";
import { Filter, IHexSignature, LogDetailed } from "src/types";
import {
getByteCodeSignatureByHash,
getDetailedTransactionLogsByField
} from "../../../../api/client";
import styled from "styled-components";
import { DisplaySignature, parseSuggestedEvent } from "../../../../web3/parseByteCode";
const TopicsContainer = styled.div`
text-align: left;
font-family: monospace;
max-width: 85%;
`
const TextEllipsis = styled.div`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`
const LinkText = styled(TextEllipsis)`
color: ${props => props.theme.global.colors.brand};
`
const NeutralMarker = styled(Box)`
border-radius: 2px;
padding: 5px;
text-align: center;
font-weight: bold;
`;
const EventsBox = styled(Box)`
padding: 10px;
font-size: small;
table {
tbody {
tr {
td {
vertical-align: top;
}
}
}
}
`
const MethodSignature = styled.div`
word-break: break-word;
font-size: 14px;
font-family: monospace;
`
const CenteredContainer = styled.div`
padding: 32px 24px;
display: flex;
justify-content: center;
align-items: center;
`
function TxsHashColumn (props: { log: LogWithSignature }) {
const { log } = props
return <div style={{ width: '140px' }}>
<Tip content={'Txn Hash'}>
<Link to={`/tx/${log.transactionHash}`}>
<LinkText>{log.transactionHash}</LinkText>
</Link>
</Tip>
<Tip content={'Block number'}>
<div style={{ display: 'flex', marginTop: '4px' }}>
#&nbsp;
<Link to={`/block/${log.blockNumber}`} style={{ display: 'inline-block' }}>
<LinkText>{log.blockNumber}</LinkText>
</Link>
</div>
</Tip>
{log.timestamp &&
<Tip content={dayjs(log.timestamp).format('MMM-DD-YYYY hh:mm:ss a') }>
<div style={{ marginTop: '4px' }}>{dayjs(log.timestamp).fromNow()}</div>
</Tip>
}
</div>
}
function TxMethod (props: { log: LogWithSignature }) {
const { inputSignatures } = props.log
return <div style={{ width: '140px' }}>
<Text size="12px">
<Tip content={'MethodID'}>
<NeutralMarker background={"backgroundBack"} width={'100px'}>
<TextEllipsis>{props.log.input.slice(0, 10)}</TextEllipsis>
</NeutralMarker>
</Tip>
</Text>
{inputSignatures && inputSignatures.length > 0 &&
<MethodSignature style={{ marginTop: '8px' }}>
{inputSignatures[0].signature}
</MethodSignature>
}
</div>
}
const Topic = styled(TextEllipsis)<{ isMarked: boolean }>`
opacity: ${(props) => props.isMarked ? 0.6 : 1};
`
function Topics (props: { log: LogWithSignature }) {
const { log } = props
const { topics, data, signatures } = log
let parsedEvents
try {
parsedEvents = signatures.map(s => s.signature).map(s => parseSuggestedEvent(s, data, topics))
} catch (err) {
}
const displaySignature = parsedEvents && parsedEvents[0] && DisplaySignature(parsedEvents[0]) || null
return <TopicsContainer>
{displaySignature &&
<div style={{ paddingBottom: '16px' }}>
{displaySignature}
</div>
}
{log.topics.map((topic, i) => {
return <Topic key={`${topic}_${i}`} isMarked={i === 0 && displaySignature}>
<Text size={'small'}>
[topic{i}] {topic}
</Text>
</Topic>
})}
</TopicsContainer>
}
const getColumns = (): ColumnConfig<LogWithSignature>[] => {
return [
{
property: "transactionHash",
header: (
<Text
color="minorText"
size="small"
style={{ fontWeight: 300, width: "160px"}}
>Txn Hash</Text>
),
render: (data) => <TxsHashColumn log={data} />,
},
{
property: "method",
header: (
<Text
color="minorText"
size="small"
style={{ fontWeight: 300, width: "160px"}}
>Method</Text>
),
render: (data) => <TxMethod log={data} />,
},
{
property: "topics",
header: (
<Text color="minorText" size="small" style={{ fontWeight: 300, textAlign: 'left' }}>
Topics
</Text>
),
render: (data) => {
return <Topics log={data} />
},
},
];
};
interface LogWithSignature extends LogDetailed {
signatures: IHexSignature[]
inputSignatures: IHexSignature[]
}
export function EventsTab(props: {
id: string;
}) {
const limitValue = 10 // localStorage.getItem("tableLimitValue");
const initFilter: Partial<Filter> = {
offset: 0,
limit: limitValue ? +limitValue : 10,
};
const [filter, setFilter] = useState<Filter>({
...(initFilter as any),
});
const [isInitialLoading, setInitialLoading] = useState(true)
const [logs, setLogs] = useState<LogWithSignature[]>([])
const loadEvents = async () => {
try {
const logs = await getDetailedTransactionLogsByField([
0,
"address",
props.id,
filter.limit,
filter.offset
]);
const logsSignatures = await Promise.all(
logs.map((l) => {
if (l.topics && l.topics.length > 0 && l.topics[0].length > 10) {
return getByteCodeSignatureByHash([l.topics[0].slice(0, 10)])
}
return []
})
)
const inputSignatures = await Promise.all(
logs.map( (l) => {
if (l.input && l.input.length > 10) {
return getByteCodeSignatureByHash([l.input.slice(0, 10)])
}
return []
})
)
const logsWithSignatures = logs.map((l, i: number) => ({
...l,
input: l.input || 'n/a',
timestamp: l.timestamp || '',
primaryKey: `${l.address}_${i}`, // key for grommet DataTable
signatures: logsSignatures[i],
inputSignatures: inputSignatures[i]
}));
setLogs(logsWithSignatures);
} catch (e) {
console.error('Cannot get logs for address', (e as Error).message)
} finally {
setInitialLoading(false)
}
}
useEffect(() => {
loadEvents()
}, [props.id])
useEffect(() => {
loadEvents()
}, [filter.offset])
if(isInitialLoading) {
return <CenteredContainer>
<Spinner />
</CenteredContainer>
}
return (
<EventsBox>
<TransactionsTable
primaryKey={'primaryKey'}
columns={getColumns()}
filter={filter}
hideCounter={true}
setFilter={setFilter}
limit={filter.limit || 10}
data={logs}
totalElements={logs.length}
step={logs.length}
noScrollTop
minWidth="1266px"
showPages={false}
textType={"event"}
paginationOptions={['10']}
/>
</EventsBox>
);
}

@ -1,6 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import dayjs from "dayjs";
import { Box, DataTable, Text, Spinner } from "grommet"; import { Box, DataTable, Text, Spinner } from "grommet";
import { Block, Filter } from "src/types"; import { Block, Filter } from "src/types";
import { useHistory, useParams } from "react-router-dom"; import { useHistory, useParams } from "react-router-dom";
@ -11,8 +9,7 @@ import {
PaginationBlockNavigator, PaginationBlockNavigator,
PaginationRecordsPerPage, PaginationRecordsPerPage,
} from "src/components/ui"; } from "src/components/ui";
import { getBlocks, getCount } from "src/api/client"; import { getBlocks } from "src/api/client";
import { ShardDropdown } from "src/components/ui/ShardDropdown";
function getColumns(props: any) { function getColumns(props: any) {
const { history, shardNumber } = props; const { history, shardNumber } = props;

@ -9,7 +9,6 @@ import {
PaginationNavigator, PaginationNavigator,
PaginationRecordsPerPage, PaginationRecordsPerPage,
TipContent, TipContent,
TokenValue,
TPaginationAction, TPaginationAction,
} from "src/components/ui"; } from "src/components/ui";
import { Erc20 } from "../../hooks/ERC20_Pool"; import { Erc20 } from "../../hooks/ERC20_Pool";

@ -3,10 +3,10 @@ import React, { useEffect, useState } from "react";
import { Box, DataTable, Spinner, Text } from "grommet"; import { Box, DataTable, Spinner, Text } from "grommet";
import { RPCTransactionHarmony } from "src/types"; import { RPCTransactionHarmony } from "src/types";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { RelativeTimer, Address } from "src/components/ui"; import { Address } from "src/components/ui";
import { getTransactions } from "src/api/client"; import { getTransactions } from "src/api/client";
import { FormNextLink } from "grommet-icons"; import { FormNextLink } from "grommet-icons";
import { DateTime } from "../../components/ui/DateTime"; import { DateTime } from "../../components/ui";
function getColumns(props: any) { function getColumns(props: any) {
const { history } = props; const { history } = props;

@ -13,8 +13,7 @@ import {
getTransactionLogsByField, getTransactionLogsByField,
getByteCodeSignatureByHash, getByteCodeSignatureByHash,
} from "src/api/client"; } from "src/api/client";
import { AllBlocksTable } from "../AllBlocksPage/AllBlocksTable"; import { revertErrorMessage } from "src/web3/parseByteCode";
import { parseSuggestedMethod, revertErrorMessage } from "src/web3/parseByteCode";
import { hmyv2_getTransactionReceipt } from "src/api/rpc"; import { hmyv2_getTransactionReceipt } from "src/api/rpc";
const extractError = (err: any) => { const extractError = (err: any) => {

@ -8,11 +8,10 @@ import {
TextInput, TextInput,
} from "grommet"; } from "grommet";
import React from "react"; import React from "react";
import { BaseContainer, BasePage, Button } from "src/components/ui"; import { BasePage, Button } from "src/components/ui";
import styled from "styled-components"; import styled from "styled-components";
import { IVerifyContractData, verifyContractCode } from "src/api/explorerV1"; import { IVerifyContractData, verifyContractCode } from "src/api/explorerV1";
import { CircleAlert, StatusGood, SubtractCircle } from "grommet-icons"; import { SubtractCircle } from "grommet-icons";
import { toaster } from "src/App";
import { breakpoints } from "../../Responive/breakpoints"; import { breakpoints } from "../../Responive/breakpoints";
import { useMediaQuery } from "react-responsive"; import { useMediaQuery } from "react-responsive";
import { useHistory } from "react-router"; import { useHistory } from "react-router";

@ -173,6 +173,11 @@ export type Log = {
removed: boolean; removed: boolean;
}; };
export interface LogDetailed extends Log {
input: string
timestamp: string
}
export type TraceCallTypes = export type TraceCallTypes =
| "CALL" | "CALL"
| "STATICCALL" | "STATICCALL"

@ -1,8 +1,8 @@
import Web3 from "web3"; import Web3 from "web3";
import { Box, Text } from "grommet"; import { Text } from "grommet";
import React from "react"; import React from "react";
import { Address } from "../components/ui"; import { Address } from "../components/ui";
import { ByteCode, IHexSignature, InternalTransaction } from "../types"; import { ByteCode, IHexSignature } from "../types";
const web3 = new Web3(); const web3 = new Web3();

Loading…
Cancel
Save