diff --git a/src/components/nav/Header.tsx b/src/components/nav/Header.tsx index 4f0cfa9..f9f46ec 100644 --- a/src/components/nav/Header.tsx +++ b/src/components/nav/Header.tsx @@ -23,7 +23,7 @@ export function Header({ pathName }: { pathName: string }) { const showSearch = !PAGES_EXCLUDING_SEARCH.includes(pathName); return ( -
+
@@ -44,9 +44,6 @@ export function Header({ pathName }: { pathName: string }) { Home - - Debugger - About diff --git a/src/features/debugger/TxDebugger.tsx b/src/features/debugger/TxDebugger.tsx index 9d69668..73799af 100644 --- a/src/features/debugger/TxDebugger.tsx +++ b/src/features/debugger/TxDebugger.tsx @@ -20,6 +20,7 @@ import { sanitizeString, toTitleCase } from '../../utils/string'; import { isValidSearchQuery } from '../messages/utils'; import { debugMessagesForHash } from './debugMessage'; +import { debugStatusToDesc } from './strings'; import { MessageDebugResult, TxDebugStatus } from './types'; const QUERY_HASH_PARAM = 'txHash'; @@ -118,7 +119,8 @@ function DebugResult({ result }: { result: MessageDebugResult | null | undefined

{`Message ${i + 1} / ${ result.messageDetails.length }`}

-

{m.summary}

+

{debugStatusToDesc[m.status]}

+

{m.details}

{Array.from(m.properties.entries()).map(([key, val]) => (
diff --git a/src/features/debugger/debugMessage.ts b/src/features/debugger/debugMessage.ts index 488c8f5..b5b9ef1 100644 --- a/src/features/debugger/debugMessage.ts +++ b/src/features/debugger/debugMessage.ts @@ -1,6 +1,6 @@ // Based on debug script in monorepo // https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/infra/scripts/debug-message.ts -import { providers } from 'ethers'; +import { BigNumber, providers } from 'ethers'; import { IMessageRecipient__factory, Inbox } from '@hyperlane-xyz/core'; import { @@ -17,9 +17,8 @@ import { Environment } from '../../consts/environments'; import { trimLeading0x } from '../../utils/addresses'; import { errorToString } from '../../utils/errors'; import { logger } from '../../utils/logger'; -import { chunk } from '../../utils/string'; +import { chunk, trimToLength } from '../../utils/string'; -import { debugStatusToDesc } from './strings'; import { LinkProperty, MessageDebugDetails, @@ -28,6 +27,9 @@ import { TxDebugStatus, } from './types'; +const HANDLE_FUNCTION_SIG = 'handle(uint32,bytes32,bytes)'; +const ETHERS_FAILURE_REASON_REGEX = /.*reason="(.*?)".*/gm; + export async function debugMessagesForHash( txHash: string, environment: Environment, @@ -49,6 +51,7 @@ export async function debugMessagesForHash( chainName, transactionReceipt, environment, + undefined, attemptGetProcessTx, multiProvider, ); @@ -58,6 +61,7 @@ export async function debugMessagesForTransaction( chainName: ChainName, txReceipt: providers.TransactionReceipt, environment: Environment, + leafIndex?: number, attemptGetProcessTx = true, multiProvider = new MultiProvider(chainConnectionConfigs), ): Promise { @@ -78,10 +82,13 @@ export async function debugMessagesForTransaction( logger.debug(`Found ${dispatchedMessages.length} messages`); const messageDetails: MessageDebugDetails[] = []; for (let i = 0; i < dispatchedMessages.length; i++) { + const msg = dispatchedMessages[i]; + if (leafIndex && !BigNumber.from(msg.leafIndex).eq(leafIndex)) { + logger.debug(`Skipping message ${i + 1}, does not match leafIndex ${leafIndex}`); + continue; + } logger.debug(`Checking message ${i + 1} of ${dispatchedMessages.length}`); - messageDetails.push( - await checkMessage(core, multiProvider, dispatchedMessages[i], attemptGetProcessTx), - ); + messageDetails.push(await checkMessage(core, multiProvider, msg, attemptGetProcessTx)); logger.debug(`Done checking message ${i + 1}`); } return { @@ -131,7 +138,7 @@ async function checkMessage( multiProvider: MultiProvider, message: DispatchedMessage, attemptGetProcessTx = true, -) { +): Promise { logger.debug(JSON.stringify(message)); const properties = new Map(); @@ -165,9 +172,7 @@ async function checkMessage( return { status: MessageDebugStatus.InvalidDestDomain, properties, - summary: `${ - debugStatusToDesc[MessageDebugStatus.InvalidDestDomain] - } Note, domain ids usually do not match chain ids. See https://docs.hyperlane.xyz/hyperlane-docs/developers/domains`, + details: `No chain found for domain ${message.parsed.destination}. Some Domain IDs do not match Chain IDs. See https://docs.hyperlane.xyz/hyperlane-docs/developers/domains`, }; } @@ -178,9 +183,7 @@ async function checkMessage( return { status: MessageDebugStatus.UnknownDestChain, properties, - summary: `${ - debugStatusToDesc[MessageDebugStatus.UnknownDestChain] - } Did you set the right environment in the top right picker? See https://docs.hyperlane.xyz/hyperlane-docs/developers/domains`, + details: `Hyperlane has multiple environments. See https://docs.hyperlane.xyz/hyperlane-docs/developers/domains`, }; } @@ -205,7 +208,7 @@ async function checkMessage( return { status: MessageDebugStatus.AlreadyProcessed, properties, - summary: debugStatusToDesc[MessageDebugStatus.AlreadyProcessed], + details: 'See delivery transaction for more details', }; } else { logger.debug('Message not yet processed'); @@ -219,9 +222,7 @@ async function checkMessage( return { status: MessageDebugStatus.RecipientNotContract, properties, - summary: `${ - debugStatusToDesc[MessageDebugStatus.AlreadyProcessed] - } Addr: ${recipientAddress}. Ensure bytes32 value is not malformed.`, + details: `Recipient address is ${recipientAddress}. Ensure that the bytes32 value is not malformed.`, }; } @@ -239,37 +240,27 @@ async function checkMessage( return { status: MessageDebugStatus.NoErrorsFound, properties, - summary: debugStatusToDesc[MessageDebugStatus.NoErrorsFound], + details: 'Message may just need more time to be processed', }; } catch (err: any) { - const messagePrefix = debugStatusToDesc[MessageDebugStatus.HandleCallFailure]; - logger.info(messagePrefix); - const errorString = errorToString(err); - logger.debug(errorString); + logger.info('Estimate gas call failed'); - // scan bytecode for handle function selector - const bytecode = await destinationProvider.getCode(recipientAddress); - const msgRecipientInterface = IMessageRecipient__factory.createInterface(); - const handleFunction = msgRecipientInterface.functions['handle(uint32,bytes32,bytes)']; - const handleSignature = msgRecipientInterface.getSighash(handleFunction); - if (!bytecode.includes(trimLeading0x(handleSignature))) { - const bytecodeMessage = `${ - debugStatusToDesc[MessageDebugStatus.RecipientNotHandler] - } ${handleSignature}. Contract may be proxied.`; - logger.info(bytecodeMessage); + const bytecodeHasHandle = await tryCheckBytecodeHandle(destinationProvider, recipientAddress); + if (!bytecodeHasHandle) { + logger.info('Bytecode does not have function matching handle sig'); return { status: MessageDebugStatus.RecipientNotHandler, properties, - // TODO format the error string better to be easier to understand - summary: `${messagePrefix}. ${bytecodeMessage}`, + details: `Recipient contract should have handle function of signature: ${HANDLE_FUNCTION_SIG}. Check that recipient is not a proxy.`, }; } + const errorReason = extractReasonString(errorToString(err, 1000)); + logger.debug(errorReason); return { status: MessageDebugStatus.HandleCallFailure, properties, - // TODO format the error string better to be easier to understand - summary: `${messagePrefix}. Details: ${errorString}`, + details: errorReason, }; } } @@ -306,3 +297,30 @@ async function tryGetProcessTxHash(destinationInbox: Inbox, messageHash: string) } return null; } + +async function tryCheckBytecodeHandle( + destinationProvider: providers.Provider, + recipientAddress: string, +) { + try { + // scan bytecode for handle function selector + const bytecode = await destinationProvider.getCode(recipientAddress); + const msgRecipientInterface = IMessageRecipient__factory.createInterface(); + const handleFunction = msgRecipientInterface.functions[HANDLE_FUNCTION_SIG]; + const handleSignature = msgRecipientInterface.getSighash(handleFunction); + return bytecode.includes(trimLeading0x(handleSignature)); + } catch (error) { + logger.error('Error checking bytecode for handle fn', error); + return true; + } +} + +function extractReasonString(errorString) { + const matches = ETHERS_FAILURE_REASON_REGEX.exec(errorString); + if (matches && matches.length >= 2) { + return `Failure reason: ${matches[1]}`; + } else { + // TODO handle more cases here as needed + return `Failure reason: ${trimToLength(errorString, 300)}`; + } +} diff --git a/src/features/debugger/types.ts b/src/features/debugger/types.ts index bffc0d8..6cb095e 100644 --- a/src/features/debugger/types.ts +++ b/src/features/debugger/types.ts @@ -34,7 +34,7 @@ export interface LinkProperty { export interface MessageDebugDetails { status: MessageDebugStatus; properties: Map; - summary: string; + details: string; } export interface DebugMessagesFoundResult { diff --git a/src/features/deliveryStatus/fetchDeliveryStatus.ts b/src/features/deliveryStatus/fetchDeliveryStatus.ts index 54872d8..9d69c91 100644 --- a/src/features/deliveryStatus/fetchDeliveryStatus.ts +++ b/src/features/deliveryStatus/fetchDeliveryStatus.ts @@ -56,8 +56,8 @@ export async function fetchDeliveryStatus( }; return result; } else { - const originTxHash = message.originTransaction.transactionHash; - const originChainId = message.originChainId; + const { originChainId, originTransaction, leafIndex } = message; + const originTxHash = originTransaction.transactionHash; const originName = chainIdToName[originChainId]; const environment = getChainEnvironment(originName); const originTxReceipt = await queryExplorerForTxReceipt(originChainId, originTxHash); @@ -67,6 +67,7 @@ export async function fetchDeliveryStatus( originName, originTxReceipt, environment, + leafIndex, false, ); @@ -87,6 +88,7 @@ export async function fetchDeliveryStatus( const result: MessageDeliveryFailingResult = { status: MessageStatus.Failing, debugStatus: firstError.status, + debugDetails: firstError.details, }; return result; } diff --git a/src/features/deliveryStatus/types.ts b/src/features/deliveryStatus/types.ts index fb07b03..d951820 100644 --- a/src/features/deliveryStatus/types.ts +++ b/src/features/deliveryStatus/types.ts @@ -13,6 +13,7 @@ export interface MessageDeliverySuccessResult extends MessageDeliveryResult { export interface MessageDeliveryFailingResult extends MessageDeliveryResult { status: MessageStatus.Failing; debugStatus: MessageDebugStatus; + debugDetails: string; } export interface MessageDeliveryPendingResult extends MessageDeliveryResult { diff --git a/src/features/deliveryStatus/useMessageDeliveryStatus.ts b/src/features/deliveryStatus/useMessageDeliveryStatus.ts index e111f52..05bc23b 100644 --- a/src/features/deliveryStatus/useMessageDeliveryStatus.ts +++ b/src/features/deliveryStatus/useMessageDeliveryStatus.ts @@ -1,13 +1,15 @@ import { useQuery } from '@tanstack/react-query'; -import { logger } from 'ethers'; +import { useEffect } from 'react'; +import { toast } from 'react-toastify'; import { Message, MessageStatus } from '../../types'; +import { logger } from '../../utils/logger'; import type { MessageDeliveryStatusResponse } from './types'; export function useMessageDeliveryStatus(message: Message, isReady: boolean) { const serializedMessage = JSON.stringify(message); - return useQuery( + const queryResult = useQuery( ['messageProcessTx', serializedMessage, isReady], async () => { if (!isReady || !message || message.status === MessageStatus.Delivered) return null; @@ -29,4 +31,14 @@ export function useMessageDeliveryStatus(message: Message, isReady: boolean) { }, { retry: false }, ); + + // Show toast on error + const error = queryResult.error; + useEffect(() => { + if (error) { + logger.error('Error fetching delivery status', error); + toast.error(`${error}`); + } + }, [error]); + return queryResult; } diff --git a/src/features/messages/MessageDetails.tsx b/src/features/messages/MessageDetails.tsx index 8db62f0..be55486 100644 --- a/src/features/messages/MessageDetails.tsx +++ b/src/features/messages/MessageDetails.tsx @@ -1,5 +1,4 @@ import Image from 'next/future/image'; -import Link from 'next/link'; import { PropsWithChildren, useCallback, useEffect, useMemo } from 'react'; import { toast } from 'react-toastify'; import { useQuery } from 'urql'; @@ -18,7 +17,7 @@ import CheckmarkIcon from '../../images/icons/checkmark-circle.svg'; import ErrorCircleIcon from '../../images/icons/error-circle.svg'; import { useStore } from '../../store'; import { Message, MessageStatus, PartialTransactionReceipt } from '../../types'; -import { getChainDisplayName, getChainEnvironment } from '../../utils/chains'; +import { getChainDisplayName } from '../../utils/chains'; import { getTxExplorerUrl } from '../../utils/explorers'; import { logger } from '../../utils/logger'; import { getDateTimeString, getHumanReadableTimeString } from '../../utils/time'; @@ -54,21 +53,23 @@ export function MessageDetails({ messageId }: { messageId: string }) { } = message; const isIcaMsg = isIcaMessage(message); - const { data: deliveryStatusResponse, error: deliveryStatusError } = useMessageDeliveryStatus( - message, - isMessageFound, - ); + const { data: deliveryStatusResponse } = useMessageDeliveryStatus(message, isMessageFound); let resolvedDestTx = destTransaction; let resolvedMsgStatus = status; - let debugStatus: MessageDebugStatus | undefined = undefined; + let debugInfo: TransactionCardDebugInfo | undefined = undefined; // If there's a delivery status response, use those values as s.o.t. instead if (deliveryStatusResponse) { resolvedMsgStatus = deliveryStatusResponse.status; if (deliveryStatusResponse.status === MessageStatus.Delivered) { resolvedDestTx = deliveryStatusResponse.deliveryTransaction; } else if (deliveryStatusResponse.status === MessageStatus.Failing) { - debugStatus = deliveryStatusResponse.debugStatus; + debugInfo = { + status: deliveryStatusResponse.debugStatus, + details: deliveryStatusResponse.debugDetails, + originChainId, + originTxHash: originTransaction.transactionHash, + }; } } @@ -86,14 +87,10 @@ export function MessageDetails({ messageId }: { messageId: string }) { } else { setBanner(''); } - - if (deliveryStatusError) { - logger.error('Error fetching delivery status', deliveryStatusError); - toast.error(`${deliveryStatusError}`); - } - + }, [error, isFetching, resolvedMsgStatus, isMessageFound, setBanner]); + useEffect(() => { return () => setBanner(''); - }, [error, deliveryStatusError, isFetching, resolvedMsgStatus, isMessageFound, setBanner]); + }, [setBanner]); const reExecutor = useCallback(() => { if (!isMessageFound || resolvedMsgStatus !== MessageStatus.Delivered) { @@ -138,7 +135,7 @@ export function MessageDetails({ messageId }: { messageId: string }) { chainId={originChainId} status={resolvedMsgStatus} transaction={originTransaction} - help={helpText.origin} + helpText={helpText.origin} shouldBlur={shouldBlur} /> @@ -187,22 +180,25 @@ interface TransactionCardProps { chainId: number; status: MessageStatus; transaction?: PartialTransactionReceipt; - debugInfo?: { - status?: MessageDebugStatus; - originChainId: number; - originTxHash: string; - }; - help: string; + debugInfo?: TransactionCardDebugInfo; + helpText: string; shouldBlur: boolean; } +interface TransactionCardDebugInfo { + status: MessageDebugStatus; + details: string; + originChainId: number; + originTxHash: string; +} + function TransactionCard({ title, chainId, status, transaction, debugInfo, - help, + helpText, shouldBlur, }: TransactionCardProps) { const txExplorerLink = getTxExplorerUrl(chainId, transaction?.transactionHash); @@ -214,7 +210,7 @@ function TransactionCard({

{title}

- +
{transaction && ( @@ -271,23 +267,17 @@ function TransactionCard({ )} {!transaction && status === MessageStatus.Failing && (
-
- Destination chain delivery transaction currently failing +
+ Destination delivery transaction currently failing
{debugInfo && ( <> -
{`Failure reason: ${ - debugInfo.status ? debugStatusToDesc[debugInfo.status] : 'Unknown' - }`}
- - - View in transaction debugger - - +
+ {debugStatusToDesc[debugInfo.status]} +
+
+ {debugInfo.details} +
)}