Skip to content
Open
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
39 changes: 11 additions & 28 deletions apps/explorer/src/comps/TxEventDescription.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { useQuery } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router'
import { type Address as AddressType, Hex, Value } from 'ox'
import * as React from 'react'
import { decodeFunctionData, isAddressEqual } from 'viem'
import { isAddressEqual } from 'viem'
import { Address } from '#comps/Address'
import { Amount } from '#comps/Amount'
import { Midcut } from '#comps/Midcut'
import { TokenIcon } from '#comps/TokenIcon'
import { cx } from '#cva.config.ts'
import { extractContractAbi, getContractAbi } from '#lib/domain/contracts.ts'
import { useLookupSignature } from '#lib/abi'
import type { KnownEvent, KnownEventPart } from '#lib/domain/known-events.ts'
import {
DateFormatter,
Expand All @@ -19,7 +18,7 @@ import {

/**
* Renders a contract call with decoded function name.
* Fetches ABI from registry or extracts from bytecode using whatsabi.
* Uses signature lookup to get the function name from the selector.
*/
function ContractCallPart(props: {
address: AddressType.Address
Expand All @@ -28,38 +27,22 @@ function ContractCallPart(props: {
const { address, input } = props
const selector = Hex.slice(input, 0, 4)

const { data: functionName, isLoading } = useQuery({
queryKey: ['contract-call-function', address, selector],
queryFn: async () => {
// Try known ABI first
let abi = getContractAbi(address)
const { data: signature } = useLookupSignature({ selector })

// Fall back to extracting from bytecode
if (!abi) {
abi = await extractContractAbi(address)
}

if (!abi) return null

try {
const decoded = decodeFunctionData({ abi, data: input })
return decoded.functionName
} catch {
return null
}
},
staleTime: Number.POSITIVE_INFINITY,
})
const functionName = React.useMemo(() => {
if (!signature) return null
const parenIndex = signature.indexOf('(')
return parenIndex > 0 ? signature.slice(0, parenIndex) : signature
}, [signature])

// Show selector while loading or if we couldn't decode
const displayText = isLoading ? selector : (functionName ?? selector)
const displayText = functionName ?? selector

return (
<Link
to="/address/$address"
params={{ address }}
search={{ tab: 'contract' }}
title={`${address} - ${functionName ?? selector}`}
title={`${address} - ${displayText}`}
className="press-down whitespace-nowrap"
>
<span className="text-accent items-end">{displayText}</span>
Expand Down
55 changes: 32 additions & 23 deletions apps/explorer/src/lib/domain/known-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -829,8 +829,8 @@ export function preferredEventsFilter(event: KnownEvent): boolean {
}

/**
* Detects a contract call when viewing a transaction from the called contract's perspective.
* Returns a KnownEvent if the viewer is the contract being called, otherwise null.
* Detects a contract call when viewing a transaction.
* Returns a KnownEvent describing the contract interaction.
*/
function detectContractCall(
receipt: TransactionReceipt,
Expand All @@ -842,16 +842,26 @@ function detectContractCall(
const { viewer } = options ?? {}
const contractAddress = receipt.to

// Only show contract call when viewing as the called contract
if (!contractAddress || !viewer || !Address.isEqual(contractAddress, viewer))
return null
if (!contractAddress) return null

const transaction = options?.transaction
const callInput = transaction?.input ?? transaction?.data

// Need input data to show a contract call
if (!callInput || callInput === '0x') return null

if (viewer && Address.isEqual(contractAddress, viewer)) {
return {
type: 'contract call',
parts: [
{ type: 'action', value: 'Call To' },
{
type: 'contractCall',
value: { address: contractAddress, input: callInput },
},
],
}
}

return {
type: 'contract call',
parts: [
Expand All @@ -860,6 +870,8 @@ function detectContractCall(
type: 'contractCall',
value: { address: contractAddress, input: callInput },
},
{ type: 'text', value: 'at' },
{ type: 'account', value: contractAddress },
],
}
}
Expand Down Expand Up @@ -1239,26 +1251,23 @@ export function parseKnownEvents(
const contractCallEvent = detectContractCall(receipt, options)
if (contractCallEvent) {
knownEvents.push(contractCallEvent)
}
}

// If no known events were parsed but there was a fee transfer,
// show it as a fee payment event
if (knownEvents.length === 0 && feeTransferEvents.length > 0) {
const parts: KnownEventPart[] = [{ type: 'action', value: 'Pay Fee' }]
} else if (feeTransferEvents.length > 0) {
// Only show "Pay Fee" if there's no contract call to display
const parts: KnownEventPart[] = [{ type: 'action', value: 'Pay Fee' }]

for (const [index, fee] of feeTransferEvents.entries()) {
if (index > 0) parts.push({ type: 'text', value: 'and' })
parts.push({
type: 'amount',
value: createAmount(fee.amount, fee.token),
})
}

for (const [index, fee] of feeTransferEvents.entries()) {
if (index > 0) parts.push({ type: 'text', value: 'and' })
parts.push({
type: 'amount',
value: createAmount(fee.amount, fee.token),
knownEvents.push({
type: 'fee',
parts,
})
}

knownEvents.push({
type: 'fee',
parts,
})
}

return knownEvents
Expand Down
9 changes: 8 additions & 1 deletion apps/explorer/src/lib/og.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,17 @@ export function buildTxDescription(
if (eventCount > 0) {
const firstEvent = txData.events[0]
const actionPart = firstEvent.parts.find((p) => p.type === 'action')
const action = actionPart
let action = actionPart
? truncateOgText(String(actionPart.value).toLowerCase(), 20)
: 'transaction'

if (firstEvent.type === 'contract call') {
const contractCallPart = firstEvent.parts.find((p) => p.type === 'contractCall')
if (contractCallPart && 'value' in contractCallPart) {
action = 'contract call'
}
}

if (eventCount === 1) {
return truncateOgText(
`A ${action} on ${date} from ${HexFormatter.truncate(txData.from as Address.Address)}. View full details on Tempo Explorer.`,
Expand Down
Loading