Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Append EVM function name to unencrypted verified contract calls #1516

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/1565.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Display EVM function name in verified contract calls
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"bignumber.js": "9.1.2",
"bip39": "^3.1.0",
"date-fns": "3.6.0",
"ethers": "^6.13.2",
"i18next": "23.11.5",
"react": "18.3.1",
"react-dom": "18.3.1",
Expand Down
1 change: 0 additions & 1 deletion src/app/components/ConsensusTransactionMethod/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ const MethodIconContent: FC<MethodIconContentProps> = ({
<Typography
sx={{
fontWeight: 'inherit',
textTransform: 'capitalize',
order: reverseLabel ? -1 : 0,
...(truncate
? {
Expand Down
34 changes: 23 additions & 11 deletions src/app/components/LongDataDisplay/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { FC, useEffect, useRef, useState } from 'react'
import React, { FC, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@mui/material/Button'
import Typography from '@mui/material/Typography'
import { styled } from '@mui/material/styles'
import { COLORS } from '../../../styles/theme/colors'
import Box from '@mui/material/Box'

const StyledButton = styled(Button)(({ theme }) => ({
color: COLORS.brandDark,
Expand All @@ -30,17 +31,30 @@ export const LongDataDisplay: FC<{ data: string; fontWeight?: number; collapsedL
data,
fontWeight = 700,
collapsedLinesNumber = 2,
}) => {
return (
<LongElementDisplay collapsedLinesNumber={collapsedLinesNumber}>
<Typography variant="mono" sx={{ fontWeight }}>
{data}
</Typography>
</LongElementDisplay>
)
}

export const LongElementDisplay: FC<{ children: React.ReactNode; collapsedLinesNumber?: number }> = ({
children,
collapsedLinesNumber = 2,
}) => {
const { t } = useTranslation()
const [isExpanded, setIsExpanded] = useState(false)
const [isOverflowing, setIsOverflowing] = useState(false)
const textRef = useRef<HTMLDivElement | null>(null)
const ref = useRef<HTMLDivElement | null>(null)
const collapsedContainerMaxHeight = collapsedLinesNumber * lineHeight

useEffect(() => {
const checkOverflow = () => {
if (textRef.current) {
const isOverflow = textRef.current.scrollHeight > textRef.current.clientHeight
if (ref.current) {
const isOverflow = ref.current.scrollHeight > ref.current.clientHeight
setIsOverflowing(isOverflow)
}
}
Expand All @@ -52,15 +66,13 @@ export const LongDataDisplay: FC<{ data: string; fontWeight?: number; collapsedL

window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [data])
}, [children])

return (
<div>
<Typography
variant="mono"
ref={textRef}
<Box
ref={ref}
sx={{
fontWeight,
maxHeight: isExpanded ? 'none' : collapsedContainerMaxHeight,
overflow: 'hidden',
lineHeight: `${lineHeight}px`,
Expand All @@ -70,8 +82,8 @@ export const LongDataDisplay: FC<{ data: string; fontWeight?: number; collapsedL
WebkitBoxOrient: 'vertical',
}}
>
{data}
</Typography>
{children}
</Box>
{(isOverflowing || isExpanded) && (
<StyledButton onClick={() => setIsExpanded(!isExpanded)}>
{isExpanded ? t('common.hide') : t('common.show')}
Expand Down
125 changes: 106 additions & 19 deletions src/app/components/RuntimeTransactionMethod/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import QuestionMarkIcon from '@mui/icons-material/QuestionMark'
import LanIcon from '@mui/icons-material/Lan'
import LanOutlinedIcon from '@mui/icons-material/LanOutlined'
import { MethodIcon } from '../ConsensusTransactionMethod'
import { RuntimeTransaction } from '../../../oasis-nexus/api'
import { AbiCoder } from 'ethers'
import { base64ToHex } from '../../utils/helpers'
import { hexToBytes } from '@ethereumjs/util'
import * as oasis from '@oasisprotocol/client'
import { LongDataDisplay } from '../LongDataDisplay'

const getRuntimeTransactionLabel = (t: TFunction, method: string | undefined) => {
switch (method) {
Expand All @@ -35,6 +41,21 @@ const getRuntimeTransactionLabel = (t: TFunction, method: string | undefined) =>
}
}

/**
* The method call body. Defined by the runtime.
*
* May be undefined if the transaction was malformed.
*
* In theory, this could be any string as the runtimes evolve.
* In practice, the nexus currently expects only the following methods:
* - "accounts.Transfer"
* - "consensus.Deposit"
* - "consensus.Withdraw"
* - "consensus.Delegate"
* - "consensus.Undelegate"
* - "evm.Create"
* - "evm.Call"
*/
const getRuntimeTransactionIcon = (method: string | undefined, label: string, truncate?: boolean) => {
const props = {
border: false,
Expand Down Expand Up @@ -63,28 +84,94 @@ const getRuntimeTransactionIcon = (method: string | undefined, label: string, tr
}

type RuntimeTransactionLabelProps = {
/**
* The method call body. Defined by the runtime.
*
* May be undefined if the transaction was malformed.
*
* In theory, this could be any string as the runtimes evolve.
* In practice, the nexus currently expects only the following methods:
* - "accounts.Transfer"
* - "consensus.Deposit"
* - "consensus.Withdraw"
* - "consensus.Delegate"
* - "consensus.Undelegate"
* - "evm.Create"
* - "evm.Call"
*/
method?: string
transaction: RuntimeTransaction
truncate?: boolean
}

export const RuntimeTransactionMethod: FC<RuntimeTransactionLabelProps> = ({ method, truncate }) => {
export const RuntimeTransactionMethod: FC<RuntimeTransactionLabelProps> = ({ transaction, truncate }) => {
const { t } = useTranslation()
const label = getRuntimeTransactionLabel(t, method)
let label = getRuntimeTransactionLabel(t, transaction.method)
if (transaction.evm_fn_name) {
if (truncate) {
label = `${transaction.evm_fn_name}`
} else {
label += `: ${transaction.evm_fn_name}`
}
}
if (transaction.to_eth === '0x0100000000000000000000000000000000000103' && transaction.body?.data) {
// Subcall precompile
try {
const coder = AbiCoder.defaultAbiCoder()
const parsed = coder.decode(['string', 'bytes'], base64ToHex(transaction.body.data))
const methodName = parsed[0]

if (truncate) {
label = `${methodName}`
} else {
label += `: ${methodName}`
}
} catch (e) {
console.error('Failed to parse subcall data (might be malformed)', e, transaction)
}
}

return <>{getRuntimeTransactionIcon(transaction.method, label, truncate)}</>
}

export const RuntimeTransactionEVMParams: FC<RuntimeTransactionLabelProps> = ({
transaction,
}: {
transaction: RuntimeTransaction
}) => {
const { t } = useTranslation()

let text = ''
if (transaction.evm_fn_name) {
text = `${transaction.evm_fn_name}(${transaction.evm_fn_params?.map(a => JSON.stringify(a.value, null, 2)).join(', ')})`
}
if (transaction.to_eth === '0x0100000000000000000000000000000000000103' && transaction.body?.data) {
// Subcall precompile
try {
const coder = AbiCoder.defaultAbiCoder()
const [methodName, cborHexArgs] = coder.decode(['string', 'bytes'], base64ToHex(transaction.body.data))

text = `${methodName}`
const rawArgs = oasis.misc.fromCBOR(hexToBytes(cborHexArgs))
if (rawArgs === null) {
text += `(null)`
} else if (typeof rawArgs === 'object' && !Array.isArray(rawArgs)) {
const jsonArgs = JSON.stringify(
rawArgs,
(key, value) => {
if (value instanceof Uint8Array) {
if (key === 'from') return oasis.staking.addressToBech32(value)
if (key === 'to') return oasis.staking.addressToBech32(value)
if (key === 'shares') return oasis.quantity.toBigInt(value).toLocaleString()
return `0x${oasis.misc.toHex(value)}`
}
if (Array.isArray(value) && value.length === 2) {
if (key === 'amount') {
return [oasis.quantity.toBigInt(value[0]).toLocaleString(), oasis.misc.toStringUTF8(value[1])]
}
}
return value
},
2,
)
text += `(${jsonArgs})`
}
} catch (e) {
console.error('Failed to parse subcall data (might be malformed)', e, transaction)
}
}

return <>{getRuntimeTransactionIcon(method, label, truncate)}</>
if (!text) return null
return (
<>
<dt>{t('transactions.method.evm.call')}</dt>
<dd>
<LongDataDisplay data={text} collapsedLinesNumber={2} fontWeight={400} />
</dd>
</>
)
}
2 changes: 1 addition & 1 deletion src/app/components/Transactions/RuntimeTransactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export const RuntimeTransactions: FC<TransactionsProps> = ({
...(verbose
? [
{
content: <RuntimeTransactionMethod method={transaction.method} truncate />,
content: <RuntimeTransactionMethod transaction={transaction} truncate />,
key: 'type',
},
{
Expand Down
13 changes: 10 additions & 3 deletions src/app/pages/RuntimeTransactionDetailPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import { PageLayout } from '../../components/PageLayout'
import { SubPageCard } from '../../components/SubPageCard'
import { StatusIcon } from '../../components/StatusIcon'
import { RuntimeTransactionMethod } from '../../components/RuntimeTransactionMethod'
import { RuntimeTransactionEVMParams, RuntimeTransactionMethod } from '../../components/RuntimeTransactionMethod'

Check warning on line 14 in src/app/pages/RuntimeTransactionDetailPage/index.tsx

View workflow job for this annotation

GitHub Actions / lint

Replace `·RuntimeTransactionEVMParams,·RuntimeTransactionMethod·` with `⏎··RuntimeTransactionEVMParams,⏎··RuntimeTransactionMethod,⏎`
import { useFormattedTimestampStringWithDistance } from '../../hooks/useFormattedTimestamp'
import { styled } from '@mui/material/styles'
import { useScreenSize } from '../../hooks/useScreensize'
Expand All @@ -30,12 +30,13 @@
import { AddressSwitch, AddressSwitchOption } from '../../components/AddressSwitch'
import { TransactionEncryptionStatus } from '../../components/TransactionEncryptionStatus'
import Typography from '@mui/material/Typography'
import { LongDataDisplay } from '../../components/LongDataDisplay'
import { LongDataDisplay, LongElementDisplay } from '../../components/LongDataDisplay'
import { getPreciseNumberFormat } from '../../../locales/getPreciseNumberFormat'
import { base64ToHex } from '../../utils/helpers'
import { DappBanner } from '../../components/DappBanner'
import { getFiatCurrencyForScope, showFiatValues } from '../../../config'
import { convertToNano, getGasPrice } from '../../utils/number-utils'
import Box from '@mui/material/Box'

type TransactionSelectionResult = {
wantedTransaction?: RuntimeTransaction
Expand Down Expand Up @@ -226,9 +227,15 @@

<dt>{t('common.type')}</dt>
<dd>
<RuntimeTransactionMethod method={transaction.method} />
<LongElementDisplay>
<Box alignItems={'start'}>
<RuntimeTransactionMethod transaction={transaction} />
</Box>
</LongElementDisplay>
</dd>

<RuntimeTransactionEVMParams transaction={transaction} />

<dt>{t('transactions.encryption.format')}</dt>
<dd>
<TransactionEncryptionStatus envelope={transaction.encryption_envelope} withText={true} />
Expand Down
4 changes: 2 additions & 2 deletions src/stories/Icons.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ConsensusTransactionMethod } from '../app/components/ConsensusTransacti
import { RuntimeTransactionMethod } from '../app/components/RuntimeTransactionMethod'
import { TokenTransferIcon } from '../app/components/Tokens/TokenTransferIcon'
import { EventTypeIcon } from '../app/components/RuntimeEvents/RuntimeEventDetails'
import { ConsensusTxMethod, RuntimeEventType } from '../oasis-nexus/api'
import { ConsensusTxMethod, RuntimeEventType, RuntimeTransaction } from '../oasis-nexus/api'
import { COLORS } from '../styles/theme/colors'

export default {
Expand Down Expand Up @@ -53,7 +53,7 @@ const RuntimeTemplate: StoryFn = () => {
gap={4}
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingBottom: 4 }}
>
<RuntimeTransactionMethod method={method} />
<RuntimeTransactionMethod transaction={{ method } as RuntimeTransaction} />
<Typography sx={{ color: COLORS.grayMedium, fontSize: '12px' }}>({method})</Typography>
</Box>
))}
Expand Down
Loading
Loading