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);
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">
<Link href="/">
<a className="flex items-center">
@ -44,9 +44,6 @@ export function Header({ pathName }: { pathName: string }) {
<Link href="/">
<a className={styles.navLink}>Home</a>
</Link>
<Link href="/debugger">
<a className={styles.navLink}>Debugger</a>
</Link>
<a className={styles.navLink} target="_blank" href={links.home} rel="noopener noreferrer">
About
</a>

@ -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
<h2 className="text-lg text-gray-600">{`Message ${i + 1} / ${
result.messageDetails.length
}`}</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">
{Array.from(m.properties.entries()).map(([key, val]) => (
<div className="flex mt-1" key={`message-${i}-prop-${key}`}>

@ -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<MessageDebugResult> {
@ -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<any>,
message: DispatchedMessage,
attemptGetProcessTx = true,
) {
): Promise<MessageDebugDetails> {
logger.debug(JSON.stringify(message));
const properties = new Map<string, string | LinkProperty>();
@ -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)}`;
}
}

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

@ -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;
}

@ -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 {

@ -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;
}

@ -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}
/>
<TransactionCard
@ -146,12 +143,8 @@ export function MessageDetails({ messageId }: { messageId: string }) {
chainId={destChainId}
status={resolvedMsgStatus}
transaction={resolvedDestTx}
debugInfo={{
status: debugStatus,
originChainId: originChainId,
originTxHash: originTransaction.transactionHash,
}}
help={helpText.destination}
debugInfo={debugInfo}
helpText={helpText.destination}
shouldBlur={shouldBlur}
/>
<DetailsCard message={message} shouldBlur={shouldBlur} />
@ -187,13 +180,16 @@ interface TransactionCardProps {
chainId: number;
status: MessageStatus;
transaction?: PartialTransactionReceipt;
debugInfo?: {
status?: MessageDebugStatus;
debugInfo?: TransactionCardDebugInfo;
helpText: string;
shouldBlur: boolean;
}
interface TransactionCardDebugInfo {
status: MessageDebugStatus;
details: string;
originChainId: number;
originTxHash: string;
};
help: string;
shouldBlur: boolean;
}
function TransactionCard({
@ -202,7 +198,7 @@ function TransactionCard({
status,
transaction,
debugInfo,
help,
helpText,
shouldBlur,
}: TransactionCardProps) {
const txExplorerLink = getTxExplorerUrl(chainId, transaction?.transactionHash);
@ -214,7 +210,7 @@ function TransactionCard({
</div>
<div className="flex items-center pb-1">
<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>
{transaction && (
@ -271,23 +267,17 @@ function TransactionCard({
)}
{!transaction && status === MessageStatus.Failing && (
<div className="flex flex-col items-center py-5">
<div className="text-gray-500">
Destination chain delivery transaction currently failing
<div className="text-gray-500 text-center">
Destination delivery transaction currently failing
</div>
{debugInfo && (
<>
<div className="mt-4 text-gray-500">{`Failure reason: ${
debugInfo.status ? debugStatusToDesc[debugInfo.status] : 'Unknown'
}`}</div>
<Link
href={`/debugger?env=${getChainEnvironment(debugInfo.originChainId)}&txHash=${
debugInfo.originTxHash
}`}
>
<a className="mt-6 block text-sm text-gray-500 pl-px underline">
View in transaction debugger
</a>
</Link>
<div className="mt-4 text-gray-500 text-center">
{debugStatusToDesc[debugInfo.status]}
</div>
<div className="mt-4 text-gray-500 text-sm max-w-sm text-center">
{debugInfo.details}
</div>
</>
)}
</div>

Loading…
Cancel
Save