Feature/add logs history (#113)
* Add events history for ERC20 contracts * Add event method decodepull/117/head
parent
d8024cf895
commit
7089ea8a26
@ -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' }}> |
||||
# |
||||
<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> |
||||
); |
||||
} |
Loading…
Reference in new issue