Merge pull request #14 from hyperlane-xyz/details-debugger-improvements

Details debugger improvements
pull/15/head
J M Rossy 2 years ago committed by GitHub
commit 0542e99e24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      src/components/nav/Header.tsx
  2. 4
      src/features/debugger/TxDebugger.tsx
  3. 90
      src/features/debugger/debugMessage.ts
  4. 2
      src/features/debugger/types.ts
  5. 6
      src/features/deliveryStatus/fetchDeliveryStatus.ts
  6. 1
      src/features/deliveryStatus/types.ts
  7. 16
      src/features/deliveryStatus/useMessageDeliveryStatus.ts
  8. 76
      src/features/messages/MessageDetails.tsx

@ -23,7 +23,7 @@ export function Header({ pathName }: { pathName: string }) {
const showSearch = !PAGES_EXCLUDING_SEARCH.includes(pathName); const showSearch = !PAGES_EXCLUDING_SEARCH.includes(pathName);
return ( return (
<header className="px-2 pt-4 pb-3 sm:pt-5 sm:pb-3 sm:pl-6 sm:pr-12 w-full"> <header className="px-2 pt-4 pb-3 sm:pt-5 sm:pb-3 sm:px-6 lg:pr-14 w-full">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Link href="/"> <Link href="/">
<a className="flex items-center"> <a className="flex items-center">
@ -44,9 +44,6 @@ export function Header({ pathName }: { pathName: string }) {
<Link href="/"> <Link href="/">
<a className={styles.navLink}>Home</a> <a className={styles.navLink}>Home</a>
</Link> </Link>
<Link href="/debugger">
<a className={styles.navLink}>Debugger</a>
</Link>
<a className={styles.navLink} target="_blank" href={links.home} rel="noopener noreferrer"> <a className={styles.navLink} target="_blank" href={links.home} rel="noopener noreferrer">
About About
</a> </a>

@ -20,6 +20,7 @@ import { sanitizeString, toTitleCase } from '../../utils/string';
import { isValidSearchQuery } from '../messages/utils'; import { isValidSearchQuery } from '../messages/utils';
import { debugMessagesForHash } from './debugMessage'; import { debugMessagesForHash } from './debugMessage';
import { debugStatusToDesc } from './strings';
import { MessageDebugResult, TxDebugStatus } from './types'; import { MessageDebugResult, TxDebugStatus } from './types';
const QUERY_HASH_PARAM = 'txHash'; const QUERY_HASH_PARAM = 'txHash';
@ -118,7 +119,8 @@ function DebugResult({ result }: { result: MessageDebugResult | null | undefined
<h2 className="text-lg text-gray-600">{`Message ${i + 1} / ${ <h2 className="text-lg text-gray-600">{`Message ${i + 1} / ${
result.messageDetails.length result.messageDetails.length
}`}</h2> }`}</h2>
<p className="mt-2 leading-relaxed">{m.summary}</p> <p className="mt-2 leading-relaxed">{debugStatusToDesc[m.status]}</p>
<p className="mt-2 leading-relaxed">{m.details}</p>
<div className="mt-2 text-sm"> <div className="mt-2 text-sm">
{Array.from(m.properties.entries()).map(([key, val]) => ( {Array.from(m.properties.entries()).map(([key, val]) => (
<div className="flex mt-1" key={`message-${i}-prop-${key}`}> <div className="flex mt-1" key={`message-${i}-prop-${key}`}>

@ -1,6 +1,6 @@
// Based on debug script in monorepo // Based on debug script in monorepo
// https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/infra/scripts/debug-message.ts // 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 { IMessageRecipient__factory, Inbox } from '@hyperlane-xyz/core';
import { import {
@ -17,9 +17,8 @@ import { Environment } from '../../consts/environments';
import { trimLeading0x } from '../../utils/addresses'; import { trimLeading0x } from '../../utils/addresses';
import { errorToString } from '../../utils/errors'; import { errorToString } from '../../utils/errors';
import { logger } from '../../utils/logger'; import { logger } from '../../utils/logger';
import { chunk } from '../../utils/string'; import { chunk, trimToLength } from '../../utils/string';
import { debugStatusToDesc } from './strings';
import { import {
LinkProperty, LinkProperty,
MessageDebugDetails, MessageDebugDetails,
@ -28,6 +27,9 @@ import {
TxDebugStatus, TxDebugStatus,
} from './types'; } from './types';
const HANDLE_FUNCTION_SIG = 'handle(uint32,bytes32,bytes)';
const ETHERS_FAILURE_REASON_REGEX = /.*reason="(.*?)".*/gm;
export async function debugMessagesForHash( export async function debugMessagesForHash(
txHash: string, txHash: string,
environment: Environment, environment: Environment,
@ -49,6 +51,7 @@ export async function debugMessagesForHash(
chainName, chainName,
transactionReceipt, transactionReceipt,
environment, environment,
undefined,
attemptGetProcessTx, attemptGetProcessTx,
multiProvider, multiProvider,
); );
@ -58,6 +61,7 @@ export async function debugMessagesForTransaction(
chainName: ChainName, chainName: ChainName,
txReceipt: providers.TransactionReceipt, txReceipt: providers.TransactionReceipt,
environment: Environment, environment: Environment,
leafIndex?: number,
attemptGetProcessTx = true, attemptGetProcessTx = true,
multiProvider = new MultiProvider(chainConnectionConfigs), multiProvider = new MultiProvider(chainConnectionConfigs),
): Promise<MessageDebugResult> { ): Promise<MessageDebugResult> {
@ -78,10 +82,13 @@ export async function debugMessagesForTransaction(
logger.debug(`Found ${dispatchedMessages.length} messages`); logger.debug(`Found ${dispatchedMessages.length} messages`);
const messageDetails: MessageDebugDetails[] = []; const messageDetails: MessageDebugDetails[] = [];
for (let i = 0; i < dispatchedMessages.length; i++) { 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}`); logger.debug(`Checking message ${i + 1} of ${dispatchedMessages.length}`);
messageDetails.push( messageDetails.push(await checkMessage(core, multiProvider, msg, attemptGetProcessTx));
await checkMessage(core, multiProvider, dispatchedMessages[i], attemptGetProcessTx),
);
logger.debug(`Done checking message ${i + 1}`); logger.debug(`Done checking message ${i + 1}`);
} }
return { return {
@ -131,7 +138,7 @@ async function checkMessage(
multiProvider: MultiProvider<any>, multiProvider: MultiProvider<any>,
message: DispatchedMessage, message: DispatchedMessage,
attemptGetProcessTx = true, attemptGetProcessTx = true,
) { ): Promise<MessageDebugDetails> {
logger.debug(JSON.stringify(message)); logger.debug(JSON.stringify(message));
const properties = new Map<string, string | LinkProperty>(); const properties = new Map<string, string | LinkProperty>();
@ -165,9 +172,7 @@ async function checkMessage(
return { return {
status: MessageDebugStatus.InvalidDestDomain, status: MessageDebugStatus.InvalidDestDomain,
properties, properties,
summary: `${ 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`,
debugStatusToDesc[MessageDebugStatus.InvalidDestDomain]
} Note, domain ids usually do not match chain ids. See https://docs.hyperlane.xyz/hyperlane-docs/developers/domains`,
}; };
} }
@ -178,9 +183,7 @@ async function checkMessage(
return { return {
status: MessageDebugStatus.UnknownDestChain, status: MessageDebugStatus.UnknownDestChain,
properties, properties,
summary: `${ details: `Hyperlane has multiple environments. See https://docs.hyperlane.xyz/hyperlane-docs/developers/domains`,
debugStatusToDesc[MessageDebugStatus.UnknownDestChain]
} Did you set the right environment in the top right picker? See https://docs.hyperlane.xyz/hyperlane-docs/developers/domains`,
}; };
} }
@ -205,7 +208,7 @@ async function checkMessage(
return { return {
status: MessageDebugStatus.AlreadyProcessed, status: MessageDebugStatus.AlreadyProcessed,
properties, properties,
summary: debugStatusToDesc[MessageDebugStatus.AlreadyProcessed], details: 'See delivery transaction for more details',
}; };
} else { } else {
logger.debug('Message not yet processed'); logger.debug('Message not yet processed');
@ -219,9 +222,7 @@ async function checkMessage(
return { return {
status: MessageDebugStatus.RecipientNotContract, status: MessageDebugStatus.RecipientNotContract,
properties, properties,
summary: `${ details: `Recipient address is ${recipientAddress}. Ensure that the bytes32 value is not malformed.`,
debugStatusToDesc[MessageDebugStatus.AlreadyProcessed]
} Addr: ${recipientAddress}. Ensure bytes32 value is not malformed.`,
}; };
} }
@ -239,37 +240,27 @@ async function checkMessage(
return { return {
status: MessageDebugStatus.NoErrorsFound, status: MessageDebugStatus.NoErrorsFound,
properties, properties,
summary: debugStatusToDesc[MessageDebugStatus.NoErrorsFound], details: 'Message may just need more time to be processed',
}; };
} catch (err: any) { } catch (err: any) {
const messagePrefix = debugStatusToDesc[MessageDebugStatus.HandleCallFailure]; logger.info('Estimate gas call failed');
logger.info(messagePrefix);
const errorString = errorToString(err);
logger.debug(errorString);
// scan bytecode for handle function selector const bytecodeHasHandle = await tryCheckBytecodeHandle(destinationProvider, recipientAddress);
const bytecode = await destinationProvider.getCode(recipientAddress); if (!bytecodeHasHandle) {
const msgRecipientInterface = IMessageRecipient__factory.createInterface(); logger.info('Bytecode does not have function matching handle sig');
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);
return { return {
status: MessageDebugStatus.RecipientNotHandler, status: MessageDebugStatus.RecipientNotHandler,
properties, properties,
// TODO format the error string better to be easier to understand details: `Recipient contract should have handle function of signature: ${HANDLE_FUNCTION_SIG}. Check that recipient is not a proxy.`,
summary: `${messagePrefix}. ${bytecodeMessage}`,
}; };
} }
const errorReason = extractReasonString(errorToString(err, 1000));
logger.debug(errorReason);
return { return {
status: MessageDebugStatus.HandleCallFailure, status: MessageDebugStatus.HandleCallFailure,
properties, properties,
// TODO format the error string better to be easier to understand details: errorReason,
summary: `${messagePrefix}. Details: ${errorString}`,
}; };
} }
} }
@ -306,3 +297,30 @@ async function tryGetProcessTxHash(destinationInbox: Inbox, messageHash: string)
} }
return null; 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)}`;
}
}

@ -34,7 +34,7 @@ export interface LinkProperty {
export interface MessageDebugDetails { export interface MessageDebugDetails {
status: MessageDebugStatus; status: MessageDebugStatus;
properties: Map<string, string | LinkProperty>; properties: Map<string, string | LinkProperty>;
summary: string; details: string;
} }
export interface DebugMessagesFoundResult { export interface DebugMessagesFoundResult {

@ -56,8 +56,8 @@ export async function fetchDeliveryStatus(
}; };
return result; return result;
} else { } else {
const originTxHash = message.originTransaction.transactionHash; const { originChainId, originTransaction, leafIndex } = message;
const originChainId = message.originChainId; const originTxHash = originTransaction.transactionHash;
const originName = chainIdToName[originChainId]; const originName = chainIdToName[originChainId];
const environment = getChainEnvironment(originName); const environment = getChainEnvironment(originName);
const originTxReceipt = await queryExplorerForTxReceipt(originChainId, originTxHash); const originTxReceipt = await queryExplorerForTxReceipt(originChainId, originTxHash);
@ -67,6 +67,7 @@ export async function fetchDeliveryStatus(
originName, originName,
originTxReceipt, originTxReceipt,
environment, environment,
leafIndex,
false, false,
); );
@ -87,6 +88,7 @@ export async function fetchDeliveryStatus(
const result: MessageDeliveryFailingResult = { const result: MessageDeliveryFailingResult = {
status: MessageStatus.Failing, status: MessageStatus.Failing,
debugStatus: firstError.status, debugStatus: firstError.status,
debugDetails: firstError.details,
}; };
return result; return result;
} }

@ -13,6 +13,7 @@ export interface MessageDeliverySuccessResult extends MessageDeliveryResult {
export interface MessageDeliveryFailingResult extends MessageDeliveryResult { export interface MessageDeliveryFailingResult extends MessageDeliveryResult {
status: MessageStatus.Failing; status: MessageStatus.Failing;
debugStatus: MessageDebugStatus; debugStatus: MessageDebugStatus;
debugDetails: string;
} }
export interface MessageDeliveryPendingResult extends MessageDeliveryResult { export interface MessageDeliveryPendingResult extends MessageDeliveryResult {

@ -1,13 +1,15 @@
import { useQuery } from '@tanstack/react-query'; 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 { Message, MessageStatus } from '../../types';
import { logger } from '../../utils/logger';
import type { MessageDeliveryStatusResponse } from './types'; import type { MessageDeliveryStatusResponse } from './types';
export function useMessageDeliveryStatus(message: Message, isReady: boolean) { export function useMessageDeliveryStatus(message: Message, isReady: boolean) {
const serializedMessage = JSON.stringify(message); const serializedMessage = JSON.stringify(message);
return useQuery( const queryResult = useQuery(
['messageProcessTx', serializedMessage, isReady], ['messageProcessTx', serializedMessage, isReady],
async () => { async () => {
if (!isReady || !message || message.status === MessageStatus.Delivered) return null; if (!isReady || !message || message.status === MessageStatus.Delivered) return null;
@ -29,4 +31,14 @@ export function useMessageDeliveryStatus(message: Message, isReady: boolean) {
}, },
{ retry: false }, { 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;
} }

@ -1,5 +1,4 @@
import Image from 'next/future/image'; import Image from 'next/future/image';
import Link from 'next/link';
import { PropsWithChildren, useCallback, useEffect, useMemo } from 'react'; import { PropsWithChildren, useCallback, useEffect, useMemo } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useQuery } from 'urql'; 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 ErrorCircleIcon from '../../images/icons/error-circle.svg';
import { useStore } from '../../store'; import { useStore } from '../../store';
import { Message, MessageStatus, PartialTransactionReceipt } from '../../types'; import { Message, MessageStatus, PartialTransactionReceipt } from '../../types';
import { getChainDisplayName, getChainEnvironment } from '../../utils/chains'; import { getChainDisplayName } from '../../utils/chains';
import { getTxExplorerUrl } from '../../utils/explorers'; import { getTxExplorerUrl } from '../../utils/explorers';
import { logger } from '../../utils/logger'; import { logger } from '../../utils/logger';
import { getDateTimeString, getHumanReadableTimeString } from '../../utils/time'; import { getDateTimeString, getHumanReadableTimeString } from '../../utils/time';
@ -54,21 +53,23 @@ export function MessageDetails({ messageId }: { messageId: string }) {
} = message; } = message;
const isIcaMsg = isIcaMessage(message); const isIcaMsg = isIcaMessage(message);
const { data: deliveryStatusResponse, error: deliveryStatusError } = useMessageDeliveryStatus( const { data: deliveryStatusResponse } = useMessageDeliveryStatus(message, isMessageFound);
message,
isMessageFound,
);
let resolvedDestTx = destTransaction; let resolvedDestTx = destTransaction;
let resolvedMsgStatus = status; 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 there's a delivery status response, use those values as s.o.t. instead
if (deliveryStatusResponse) { if (deliveryStatusResponse) {
resolvedMsgStatus = deliveryStatusResponse.status; resolvedMsgStatus = deliveryStatusResponse.status;
if (deliveryStatusResponse.status === MessageStatus.Delivered) { if (deliveryStatusResponse.status === MessageStatus.Delivered) {
resolvedDestTx = deliveryStatusResponse.deliveryTransaction; resolvedDestTx = deliveryStatusResponse.deliveryTransaction;
} else if (deliveryStatusResponse.status === MessageStatus.Failing) { } 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 { } else {
setBanner(''); setBanner('');
} }
}, [error, isFetching, resolvedMsgStatus, isMessageFound, setBanner]);
if (deliveryStatusError) { useEffect(() => {
logger.error('Error fetching delivery status', deliveryStatusError);
toast.error(`${deliveryStatusError}`);
}
return () => setBanner(''); return () => setBanner('');
}, [error, deliveryStatusError, isFetching, resolvedMsgStatus, isMessageFound, setBanner]); }, [setBanner]);
const reExecutor = useCallback(() => { const reExecutor = useCallback(() => {
if (!isMessageFound || resolvedMsgStatus !== MessageStatus.Delivered) { if (!isMessageFound || resolvedMsgStatus !== MessageStatus.Delivered) {
@ -138,7 +135,7 @@ export function MessageDetails({ messageId }: { messageId: string }) {
chainId={originChainId} chainId={originChainId}
status={resolvedMsgStatus} status={resolvedMsgStatus}
transaction={originTransaction} transaction={originTransaction}
help={helpText.origin} helpText={helpText.origin}
shouldBlur={shouldBlur} shouldBlur={shouldBlur}
/> />
<TransactionCard <TransactionCard
@ -146,12 +143,8 @@ export function MessageDetails({ messageId }: { messageId: string }) {
chainId={destChainId} chainId={destChainId}
status={resolvedMsgStatus} status={resolvedMsgStatus}
transaction={resolvedDestTx} transaction={resolvedDestTx}
debugInfo={{ debugInfo={debugInfo}
status: debugStatus, helpText={helpText.destination}
originChainId: originChainId,
originTxHash: originTransaction.transactionHash,
}}
help={helpText.destination}
shouldBlur={shouldBlur} shouldBlur={shouldBlur}
/> />
<DetailsCard message={message} shouldBlur={shouldBlur} /> <DetailsCard message={message} shouldBlur={shouldBlur} />
@ -187,13 +180,16 @@ interface TransactionCardProps {
chainId: number; chainId: number;
status: MessageStatus; status: MessageStatus;
transaction?: PartialTransactionReceipt; transaction?: PartialTransactionReceipt;
debugInfo?: { debugInfo?: TransactionCardDebugInfo;
status?: MessageDebugStatus; helpText: string;
shouldBlur: boolean;
}
interface TransactionCardDebugInfo {
status: MessageDebugStatus;
details: string;
originChainId: number; originChainId: number;
originTxHash: string; originTxHash: string;
};
help: string;
shouldBlur: boolean;
} }
function TransactionCard({ function TransactionCard({
@ -202,7 +198,7 @@ function TransactionCard({
status, status,
transaction, transaction,
debugInfo, debugInfo,
help, helpText,
shouldBlur, shouldBlur,
}: TransactionCardProps) { }: TransactionCardProps) {
const txExplorerLink = getTxExplorerUrl(chainId, transaction?.transactionHash); const txExplorerLink = getTxExplorerUrl(chainId, transaction?.transactionHash);
@ -214,7 +210,7 @@ function TransactionCard({
</div> </div>
<div className="flex items-center pb-1"> <div className="flex items-center pb-1">
<h3 className="text-gray-500 font-medium text-md mr-2">{title}</h3> <h3 className="text-gray-500 font-medium text-md mr-2">{title}</h3>
<HelpIcon size={16} text={help} /> <HelpIcon size={16} text={helpText} />
</div> </div>
</div> </div>
{transaction && ( {transaction && (
@ -271,23 +267,17 @@ function TransactionCard({
)} )}
{!transaction && status === MessageStatus.Failing && ( {!transaction && status === MessageStatus.Failing && (
<div className="flex flex-col items-center py-5"> <div className="flex flex-col items-center py-5">
<div className="text-gray-500"> <div className="text-gray-500 text-center">
Destination chain delivery transaction currently failing Destination delivery transaction currently failing
</div> </div>
{debugInfo && ( {debugInfo && (
<> <>
<div className="mt-4 text-gray-500">{`Failure reason: ${ <div className="mt-4 text-gray-500 text-center">
debugInfo.status ? debugStatusToDesc[debugInfo.status] : 'Unknown' {debugStatusToDesc[debugInfo.status]}
}`}</div> </div>
<Link <div className="mt-4 text-gray-500 text-sm max-w-sm text-center">
href={`/debugger?env=${getChainEnvironment(debugInfo.originChainId)}&txHash=${ {debugInfo.details}
debugInfo.originTxHash </div>
}`}
>
<a className="mt-6 block text-sm text-gray-500 pl-px underline">
View in transaction debugger
</a>
</Link>
</> </>
)} )}
</div> </div>

Loading…
Cancel
Save