Add PI support for msg debugging and delivery status fetching

Remove old TxDebugger page and cleanup debugMessage code
Remove Environment related code only used by TxDebugger page
pull/38/head
J M Rossy 2 years ago
parent c64d6ad196
commit 65896ee699
  1. 46
      src/components/nav/EnvironmentSelector.tsx
  2. 5
      src/features/chains/ConfigureChains.tsx
  3. 11
      src/features/chains/MissingChainConfigToast.tsx
  4. 1
      src/features/chains/chainConfig.ts
  5. 7
      src/features/chains/useChainConfigs.ts
  6. 31
      src/features/chains/utils.ts
  7. 175
      src/features/debugger/TxDebugger.tsx
  8. 324
      src/features/debugger/debugMessage.ts
  9. 2
      src/features/debugger/strings.ts
  10. 3
      src/features/debugger/types.ts
  11. 21
      src/features/deliveryStatus/fetchDeliveryStatus.ts
  12. 19
      src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  13. 1
      src/features/messages/MessageDetails.tsx
  14. 13
      src/features/messages/cards/TransactionCard.tsx
  15. 4
      src/features/messages/pi-queries/usePiChainMessageQuery.ts
  16. 1
      src/pages/api/latest-nonce.ts
  17. 21
      src/pages/debugger.tsx
  18. 5
      src/store.ts
  19. 2
      src/utils/string.ts

@ -1,46 +0,0 @@
import { useEffect } from 'react';
import { Environment, environments } from '../../consts/environments';
import { useStore } from '../../store';
import { replacePathParam, useQueryParam } from '../../utils/queryParams';
import { toTitleCase } from '../../utils/string';
import { SelectField } from '../input/SelectField';
export function EnvironmentSelector() {
const { environment, setEnvironment } = useStore((s) => ({
environment: s.environment,
setEnvironment: s.setEnvironment,
}));
const queryEnv = useQueryParam('env');
useEffect(() => {
if (!queryEnv || queryEnv === environment) return;
if (environments.includes(queryEnv as Environment)) {
setEnvironment(queryEnv as Environment);
}
// Only run once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onSelect = (env: string) => {
setEnvironment(env as Environment);
replacePathParam('env', env);
};
return (
<div className="relative">
{/* <Image src={HubIcon} width={20} height={20} className="opacity-70" /> */}
<SelectField
classes="w-28 text-gray-600 border-gray-600 bg-gray-50 text-[0.95rem]"
options={envOptions}
value={environment}
onValueSelect={onSelect}
/>
</div>
);
}
const envOptions = [
{ value: Environment.Mainnet, display: toTitleCase(Environment.Mainnet) },
{ value: Environment.Testnet, display: toTitleCase(Environment.Testnet) },
];

@ -12,11 +12,11 @@ import { links } from '../../consts/links';
import { useMultiProvider } from '../providers/multiProvider';
import { tryParseChainConfig } from './chainConfig';
import { useChainConfigs } from './useChainConfigs';
import { useChainConfigsRW } from './useChainConfigs';
import { getChainDisplayName } from './utils';
export function ConfigureChains() {
const { chainConfigs, setChainConfigs } = useChainConfigs();
const { chainConfigs, setChainConfigs } = useChainConfigsRW();
const multiProvider = useMultiProvider();
const [showAddChainModal, setShowAddChainModal] = useState(false);
@ -191,7 +191,6 @@ const customChainTextareaPlaceholder = `{
"blocks": { "confirmations": 1, "estimateBlockTime": 13 },
"contracts": {
"mailbox": "0x123...",
"interchainSecurityModule": "0x123...",
"interchainGasPaymaster": "0x123..."
}
}

@ -7,13 +7,14 @@ export function MissingChainConfigToast({
chainId: number;
domainId: number;
}) {
const errorDesc = chainId
? `chain ID: ${chainId}`
: domainId
? `domain ID: ${domainId}`
: 'unknown message chain';
return (
<div>
<span>
{chainId
? `No chain config found for chain ID: {chainId}. `
: `No known chain ID for domain ${domainId}. `}
</span>
<span>{`No chain config found for ${errorDesc}. `}</span>
<Link href="/settings" className="underline">
Add a config
</Link>

@ -6,7 +6,6 @@ import { logger } from '../../utils/logger';
export const chainContractsSchema = z.object({
mailbox: z.string(),
interchainSecurityModule: z.string().optional(),
interchainGasPaymaster: z.string().optional(),
// interchainAccountRouter: z.string().optional(),
});

@ -16,6 +16,11 @@ const ChainMetadataArraySchema = z.array(ChainMetadataSchema);
// Use the chainConfigs from the store
export function useChainConfigs() {
return useStore((s) => s.chainConfigsV2);
}
// Use the chainConfigs and setChainConfigs from the store (i.e. Read/Write)
export function useChainConfigsRW() {
return useStore((s) => ({
chainConfigs: s.chainConfigsV2,
setChainConfigs: s.setChainConfigs,
@ -25,7 +30,7 @@ export function useChainConfigs() {
// Look for chainConfigs in the query string and merge them into the store
// Not to be used directly, should only require a single use in ChainConfigSyncer
export function useQueryParamChainConfigSync() {
const { chainConfigs: storeConfigs, setChainConfigs } = useChainConfigs();
const { chainConfigs: storeConfigs, setChainConfigs } = useChainConfigsRW();
const queryVal = useQueryParam(CHAIN_CONFIGS_KEY);
useEffect(() => {

@ -1,8 +1,15 @@
import { MultiProvider } from '@hyperlane-xyz/sdk';
import {
type ChainMap,
type ChainName,
type MultiProvider,
hyperlaneContractAddresses,
} from '@hyperlane-xyz/sdk';
import { Environment } from '../../consts/environments';
import { toTitleCase } from '../../utils/string';
import type { ChainConfig } from './chainConfig';
export function getChainName(mp: MultiProvider, chainId?: number) {
return mp.tryGetChainName(chainId || 0) || undefined;
}
@ -18,3 +25,25 @@ export function getChainEnvironment(mp: MultiProvider, chainIdOrName: number | s
const isTestnet = mp.tryGetChainMetadata(chainIdOrName)?.isTestnet;
return isTestnet ? Environment.Testnet : Environment.Mainnet;
}
export function tryGetContractAddress(
customChainConfigs: ChainMap<ChainConfig>,
chainName: ChainName,
contractName: keyof ChainConfig['contracts'],
): Address | undefined {
return (
customChainConfigs[chainName]?.contracts?.[contractName] ||
hyperlaneContractAddresses[chainName]?.[contractName] ||
undefined
);
}
export function getContractAddress(
customChainConfigs: ChainMap<ChainConfig>,
chainName: ChainName,
contractName: keyof ChainConfig['contracts'],
): Address {
const addr = tryGetContractAddress(customChainConfigs, chainName, contractName);
if (!addr) throw new Error(`No contract address found for ${contractName} on ${chainName}`);
return addr;
}

@ -1,175 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import Image from 'next/image';
import { useState } from 'react';
import { Fade } from '../../components/animation/Fade';
import { EnvironmentSelector } from '../../components/nav/EnvironmentSelector';
import { SearchBar } from '../../components/search/SearchBar';
import {
NoSearchError,
SearchEmptyError,
SearchInvalidError,
SearchUnknownError,
} from '../../components/search/SearchStates';
import ShrugIcon from '../../images/icons/shrug.svg';
import { useStore } from '../../store';
import useDebounce from '../../utils/debounce';
import { replacePathParam, useQueryParam } from '../../utils/queryParams';
import { sanitizeString, toTitleCase } from '../../utils/string';
import { isValidSearchQuery } from '../messages/queries/useMessageQuery';
import { useMultiProvider } from '../providers/multiProvider';
import { debugMessagesForHash } from './debugMessage';
import { debugStatusToDesc } from './strings';
import { MessageDebugResult, TxDebugStatus } from './types';
const QUERY_HASH_PARAM = 'txHash';
export function TxDebugger() {
const environment = useStore((s) => s.environment);
const multiProvider = useMultiProvider();
const txHash = useQueryParam(QUERY_HASH_PARAM);
// Search text input
const [searchInput, setSearchInput] = useState(txHash);
const debouncedSearchInput = useDebounce(searchInput, 750);
const hasInput = !!debouncedSearchInput;
const sanitizedInput = sanitizeString(debouncedSearchInput);
const isValidInput = isValidSearchQuery(sanitizedInput, false);
const {
isLoading: isFetching,
isError,
data,
} = useQuery(
['debugMessage', isValidInput, sanitizedInput, environment],
() => {
if (!isValidInput || !sanitizedInput) {
replacePathParam(QUERY_HASH_PARAM, '');
return null;
}
replacePathParam(QUERY_HASH_PARAM, sanitizedInput);
return debugMessagesForHash(sanitizedInput, environment, multiProvider);
},
{ retry: false },
);
return (
<>
<SearchBar
value={searchInput}
onChangeValue={setSearchInput}
isFetching={isFetching}
placeholder="Search transaction hash to debug message"
/>
<div className="w-full h-[38.2rem] mt-5 bg-white shadow-md border rounded overflow-auto relative">
<div className="px-2 py-3 sm:px-4 md:px-5 flex items-center justify-between border-b border-gray-100">
<h2 className="text-gray-600">{`Transaction Debugger`}</h2>
<EnvironmentSelector />
</div>
<Fade show={isValidInput && !isError && !!data}>
<div className="px-2 sm:px-4 md:px-5">
<DebugResult result={data} />
</div>
</Fade>
<SearchEmptyError
show={isValidInput && !isError && !isFetching && !data}
hasInput={hasInput}
allowAddress={false}
/>
<NoSearchError show={!hasInput && !isError} />
<SearchInvalidError show={hasInput && !isError && !isValidInput} allowAddress={false} />
<SearchUnknownError show={hasInput && isError} />
</div>
</>
);
}
function DebugResult({ result }: { result: MessageDebugResult | null | undefined }) {
if (!result) return null;
if (result.status === TxDebugStatus.NotFound) {
return (
<div className="py-12 flex flex-col items-center">
<Image src={ShrugIcon} width={110} className="opacity-80" alt="" />
<h2 className="mt-4 text-lg text-gray-600">No transaction found</h2>
<p className="mt-4 leading-relaxed">{result.details}</p>
</div>
);
}
if (result.status === TxDebugStatus.NoMessages) {
return (
<div className="py-12 flex flex-col items-center">
<Image src={ShrugIcon} width={110} className="opacity-80" alt="" />
<h2 className="mt-4 text-lg text-gray-600">No message found</h2>
<p className="mt-4 leading-relaxed">{result.details}</p>
<TxExplorerLink href={result.explorerLink} chainName={result.chainName} />
</div>
);
}
if (result.status === TxDebugStatus.MessagesFound) {
return (
<>
{result.messageDetails.map((m, i) => (
<div className="border-b border-gray-200 py-4" key={`message-${i}`}>
<h2 className="text-lg text-gray-600">{`Message ${i + 1} / ${
result.messageDetails.length
}`}</h2>
<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}`}>
<label className="text-gray-600 w-32">{key}</label>
{typeof val === 'string' ? (
<div className="relative ml-2 truncate max-w-xs sm:max-w-sm md:max-w-lg">
{val}
</div>
) : (
<div className="relative ml-2 truncate max-w-xs sm:max-w-sm md:max-w-lg">
<a
className="my-5 text-blue-600 hover:text-blue-500 underline underline-offset-4"
href={val.url}
target="_blank"
rel="noopener noreferrer"
>
{val.text}
</a>
</div>
)}
</div>
))}
</div>
</div>
))}
<TxExplorerLink href={result.explorerLink} chainName={result.chainName} />
</>
);
}
return null;
}
function TxExplorerLink({
href,
chainName,
}: {
href: string | undefined;
chainName: string | undefined;
}) {
if (!href || !chainName) return null;
return (
<a
className="block my-5 text-blue-600 hover:text-blue-500 underline underline-offset-4"
href={href}
target="_blank"
rel="noopener noreferrer"
>
{`View transaction in ${toTitleCase(chainName)} explorer`}
</a>
);
}

@ -1,181 +1,35 @@
// Forked from debug script in monorepo
// Forked from debug script in monorepo but mostly rewritten
// https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/infra/scripts/debug-message.ts
import { BigNumber, providers } from 'ethers';
import {
type IInterchainGasPaymaster,
IMessageRecipient__factory,
type Mailbox,
InterchainGasPaymaster__factory,
} from '@hyperlane-xyz/core';
import {
ChainName,
CoreChainName,
DispatchedMessage,
HyperlaneCore,
HyperlaneIgp,
MultiProvider,
TestChains,
} from '@hyperlane-xyz/sdk';
import type { ChainMap, MultiProvider } from '@hyperlane-xyz/sdk';
import { utils } from '@hyperlane-xyz/utils';
import { Environment } from '../../consts/environments';
import { Message } from '../../types';
import { trimLeading0x } from '../../utils/addresses';
import { errorToString } from '../../utils/errors';
import { logger } from '../../utils/logger';
import { chunk, trimToLength } from '../../utils/string';
import { getChainEnvironment } from '../chains/utils';
import { trimToLength } from '../../utils/string';
import type { ChainConfig } from '../chains/chainConfig';
import { getContractAddress, tryGetContractAddress } from '../chains/utils';
import { isIcaMessage, tryDecodeIcaBody, tryFetchIcaAddress } from '../messages/ica';
import {
LinkProperty,
MessageDebugDetails,
MessageDebugResult,
MessageDebugStatus,
TxDebugStatus,
} from './types';
const HANDLE_FUNCTION_SIG = 'handle(uint32,bytes32,bytes)';
export async function debugMessagesForHash(
txHash: string,
environment: Environment,
multiProvider: MultiProvider,
): Promise<MessageDebugResult> {
const txDetails = await findTransactionDetails(txHash, multiProvider);
if (!txDetails?.transactionReceipt) {
return {
status: TxDebugStatus.NotFound,
details: 'No transaction found for this hash on any supported networks.',
};
}
const { chainName, transactionReceipt } = txDetails;
return debugMessagesForTransaction(chainName, transactionReceipt, environment, multiProvider);
}
export async function debugMessagesForTransaction(
chainName: ChainName,
txReceipt: providers.TransactionReceipt,
environment: Environment,
multiProvider: MultiProvider,
nonce?: number,
): Promise<MessageDebugResult> {
// TODO PI support here
const core = HyperlaneCore.fromEnvironment(environment, multiProvider);
const dispatchedMessages = core.getDispatchedMessages(txReceipt);
const explorerLink =
multiProvider.tryGetExplorerTxUrl(chainName, {
hash: txReceipt.transactionHash,
}) || undefined;
if (!dispatchedMessages?.length) {
return {
status: TxDebugStatus.NoMessages,
details:
'No messages found for this transaction. Please check that the hash and environment are set correctly.',
chainName,
explorerLink,
};
}
logger.debug(`Found ${dispatchedMessages.length} messages`);
const messageDetails: MessageDebugDetails[] = [];
for (let i = 0; i < dispatchedMessages.length; i++) {
const msg = dispatchedMessages[i];
if (nonce && !BigNumber.from(msg.parsed.nonce).eq(nonce)) {
logger.debug(`Skipping message ${i + 1}, does not match nonce ${nonce}`);
continue;
}
logger.debug(`Checking message ${i + 1} of ${dispatchedMessages.length}`);
messageDetails.push(await debugDispatchedMessage(environment, core, multiProvider, msg));
logger.debug(`Done checking message ${i + 1}`);
}
return {
status: TxDebugStatus.MessagesFound,
chainName,
explorerLink,
messageDetails,
};
}
async function debugDispatchedMessage(
environment: Environment,
core: HyperlaneCore,
multiProvider: MultiProvider,
message: DispatchedMessage,
): Promise<MessageDebugDetails> {
const {
sender: senderBytes,
recipient: recipientBytes,
origin: originDomain,
destination: destDomain,
body,
nonce,
} = message.parsed;
const messageId = utils.messageId(message.message);
const senderAddr = utils.bytes32ToAddress(senderBytes.toString());
const recipientAddr = utils.bytes32ToAddress(recipientBytes.toString());
const originName = multiProvider.getChainName(originDomain);
const destName = multiProvider.tryGetChainName(destDomain)!;
const properties = new Map<string, string | LinkProperty>();
properties.set('ID', messageId);
properties.set('Sender', senderAddr);
properties.set('Recipient', recipientAddr);
properties.set('Origin Domain', originDomain.toString());
properties.set('Origin Chain', originName);
properties.set('Destination Domain', destDomain.toString());
properties.set('Destination Chain', destName || 'Unknown');
properties.set('Nonce', nonce.toString());
properties.set('Raw Bytes', message.message);
import { MessageDebugDetails, MessageDebugStatus } from './types';
const destInvalid = isInvalidDestDomain(core, destDomain, destName);
if (destInvalid) return { ...destInvalid, properties };
type Provider = providers.Provider;
const messageDelivered = await isMessageAlreadyDelivered(
core,
multiProvider,
destName,
messageId,
properties,
);
if (messageDelivered) return { ...messageDelivered, properties };
const destProvider = multiProvider.getProvider(destName);
const recipInvalid = await isInvalidRecipient(destProvider, recipientAddr);
if (recipInvalid) return { ...recipInvalid, properties };
const deliveryResult = await debugMessageDelivery(
core,
originDomain,
destName,
senderAddr,
recipientAddr,
senderBytes,
body,
destProvider,
);
if (deliveryResult.status && deliveryResult.details) return { ...deliveryResult, properties };
const gasEstimate = deliveryResult.gasEstimate;
const insufficientGas = await isIgpUnderfunded(
environment,
multiProvider,
messageId,
originName,
gasEstimate,
);
if (insufficientGas) return { ...insufficientGas, properties };
return noErrorFound(properties);
}
const HANDLE_FUNCTION_SIG = 'handle(uint32,bytes32,bytes)';
export async function debugExplorerMessage(
message: Message,
multiProvider: MultiProvider,
): Promise<Omit<MessageDebugDetails, 'properties'>> {
customChainConfigs: ChainMap<ChainConfig>,
message: Message,
): Promise<MessageDebugDetails> {
const {
msgId,
sender,
@ -189,36 +43,35 @@ export async function debugExplorerMessage(
const originName = multiProvider.getChainName(originDomain);
const destName = multiProvider.tryGetChainName(destDomain)!;
const environment = getChainEnvironment(multiProvider, originName);
// TODO PI support here
const core = HyperlaneCore.fromEnvironment(environment, multiProvider);
const destInvalid = isInvalidDestDomain(core, destDomain, destName);
if (destInvalid) return destInvalid;
const originProvider = multiProvider.getProvider(originDomain);
const destProvider = multiProvider.getProvider(destDomain);
const destProvider = multiProvider.getProvider(destName);
const recipInvalid = await isInvalidRecipient(destProvider, recipient);
if (recipInvalid) return recipInvalid;
const destMailbox = getContractAddress(customChainConfigs, destName, 'mailbox');
const senderBytes = utils.addressToBytes32(sender);
const deliveryResult = await debugMessageDelivery(
core,
originDomain,
destName,
destMailbox,
destProvider,
sender,
recipient,
senderBytes,
body,
destProvider,
);
if (deliveryResult.status && deliveryResult.details) return deliveryResult;
const gasEstimate = deliveryResult.gasEstimate;
const igpAddress = tryGetContractAddress(
customChainConfigs,
originName,
'interchainGasPaymaster',
);
const insufficientGas = await isIgpUnderfunded(
environment,
multiProvider,
msgId,
originName,
originProvider,
igpAddress,
gasEstimate,
totalGasAmount,
);
@ -227,103 +80,7 @@ export async function debugExplorerMessage(
return noErrorFound();
}
async function findTransactionDetails(txHash: string, multiProvider: MultiProvider) {
const chains = multiProvider
.getKnownChainNames()
.filter((n) => !TestChains.includes(n as CoreChainName));
const chainChunks = chunk(chains, 10);
for (const chunk of chainChunks) {
try {
const queries = chunk.map((c) => fetchTransactionDetails(txHash, multiProvider, c));
const result = await Promise.any(queries);
return result;
} catch (error) {
logger.debug('Tx not found, trying next chunk');
}
}
logger.debug('Tx not found on any networks');
return null;
}
async function fetchTransactionDetails(
txHash: string,
multiProvider: MultiProvider,
chainName: ChainName,
) {
const provider = multiProvider.getProvider(chainName);
// Note: receipt is null if tx not found
const transactionReceipt = await provider.getTransactionReceipt(txHash);
if (transactionReceipt) {
logger.info('Tx found', txHash, chainName);
return { chainName, transactionReceipt };
} else {
logger.debug('Tx not found', txHash, chainName);
throw new Error(`Tx not found on ${chainName}`);
}
}
function isInvalidDestDomain(core: HyperlaneCore, destDomain: DomainId, destName: string | null) {
logger.debug(`Destination chain: ${destName}`);
if (!destName) {
logger.info(`Unknown destination domain ${destDomain}`);
return {
status: MessageDebugStatus.InvalidDestDomain,
details: `No chain found for domain ${destDomain}. Some Domain IDs do not match Chain IDs. See https://docs.hyperlane.xyz/docs/resources/domains`,
};
}
if (!core.knownChain(destName)) {
logger.info(`Destination chain ${destName} unknown for environment`);
return {
status: MessageDebugStatus.UnknownDestChain,
details: `Hyperlane has multiple environments. See https://docs.hyperlane.xyz/docs/resources/domains`,
};
}
return false;
}
async function isMessageAlreadyDelivered(
core: HyperlaneCore,
multiProvider: MultiProvider,
destName: string,
messageId: string,
properties: MessageDebugDetails['properties'],
) {
const destMailbox = core.getContracts(destName).mailbox;
const isDelivered = await destMailbox.delivered(messageId);
if (isDelivered) {
logger.info('Message has already been processed');
const processTxHash = await tryGetProcessTxHash(destMailbox, messageId);
if (processTxHash) {
const url = multiProvider.tryGetExplorerTxUrl(destName, { hash: processTxHash });
properties.set('Process TX', { url: url || 'UNKNOWN', text: processTxHash });
}
return {
status: MessageDebugStatus.AlreadyProcessed,
properties,
details: 'See delivery transaction for more details',
};
}
logger.debug('Message not yet processed');
return false;
}
async function tryGetProcessTxHash(mailbox: Mailbox, messageId: string) {
try {
const filter = mailbox.filters.ProcessId(messageId);
const matchedEvents = await mailbox.queryFilter(filter);
if (matchedEvents?.length) {
const event = matchedEvents[0];
return event.transactionHash;
}
} catch (error) {
logger.error('Error finding process transaction', error);
}
return null;
}
async function isInvalidRecipient(provider: providers.Provider, recipient: Address) {
async function isInvalidRecipient(provider: Provider, recipient: Address) {
const recipientIsContract = await isContract(provider, recipient);
if (!recipientIsContract) {
logger.info(`Recipient address ${recipient} is not a contract`);
@ -335,22 +92,20 @@ async function isInvalidRecipient(provider: providers.Provider, recipient: Addre
return false;
}
async function isContract(provider: providers.Provider, address: Address) {
async function isContract(provider: Provider, address: Address) {
const code = await provider.getCode(address);
return code && code !== '0x'; // "Empty" code
}
async function debugMessageDelivery(
core: HyperlaneCore,
originDomain: DomainId,
destName: string,
destMailbox: Address,
destProvider: Provider,
sender: Address,
recipient: Address,
senderBytes: string,
body: string,
destProvider: providers.Provider,
) {
const destMailbox = core.getContracts(destName).mailbox;
const recipientContract = IMessageRecipient__factory.connect(recipient, destProvider);
try {
// TODO add special case for Arbitrum:
@ -361,7 +116,7 @@ async function debugMessageDelivery(
senderBytes,
body,
{
from: destMailbox.address,
from: destMailbox,
},
);
logger.debug(
@ -398,15 +153,17 @@ async function debugMessageDelivery(
}
async function isIgpUnderfunded(
env: Environment,
multiProvider: MultiProvider,
msgId: string,
originName: string,
originProvider: Provider,
igpAddress?: Address,
deliveryGasEst?: string,
totalGasAmount?: string,
) {
const igp = HyperlaneIgp.fromEnvironment(env, multiProvider);
const igpContract = igp.getContracts(originName).interchainGasPaymaster;
if (!igpAddress) {
logger.debug('No IGP address provided, skipping gas funding check');
return false;
}
const igpContract = InterchainGasPaymaster__factory.connect(igpAddress, originProvider);
const { isFunded, igpDetails } = await tryCheckIgpGasFunded(
igpContract,
msgId,
@ -463,7 +220,7 @@ async function tryCheckIgpGasFunded(
}
}
async function tryCheckBytecodeHandle(provider: providers.Provider, recipientAddress: string) {
async function tryCheckBytecodeHandle(provider: Provider, recipientAddress: string) {
try {
// scan bytecode for handle function selector
const bytecode = await provider.getCode(recipientAddress);
@ -482,7 +239,7 @@ async function tryDebugIcaMsg(
recipient: Address,
body: string,
originDomainId: DomainId,
destinationProvider: providers.Provider,
destinationProvider: Provider,
) {
if (!isIcaMessage({ sender, recipient })) return null;
logger.debug('Message is for an ICA');
@ -516,7 +273,7 @@ async function tryCheckIcaCall(
icaAddress: string,
destinationAddress: string,
callBytes: string,
destinationProvider: providers.Provider,
destinationProvider: Provider,
) {
try {
await destinationProvider.estimateGas({
@ -546,10 +303,9 @@ function extractReasonString(rawError: any) {
}
}
function noErrorFound(properties?: MessageDebugDetails['properties']): MessageDebugDetails {
function noErrorFound(): MessageDebugDetails {
return {
status: MessageDebugStatus.NoErrorsFound,
details: 'Message may just need more time to be processed',
properties: properties || new Map(),
};
}

@ -3,8 +3,6 @@ import { MessageDebugStatus } from './types';
export const debugStatusToDesc: Record<MessageDebugStatus, string> = {
[MessageDebugStatus.AlreadyProcessed]: 'No errors found, message already processed',
[MessageDebugStatus.NoErrorsFound]: 'No errors found, message appears to be deliverable',
[MessageDebugStatus.InvalidDestDomain]: 'The destination domain id is invalid',
[MessageDebugStatus.UnknownDestChain]: `Destination chain is not in this message's environment`,
[MessageDebugStatus.RecipientNotContract]: 'Recipient address is not a contract',
[MessageDebugStatus.RecipientNotHandler]:
'Recipient bytecode is missing handle function selector',

@ -7,8 +7,6 @@ export enum TxDebugStatus {
export enum MessageDebugStatus {
AlreadyProcessed = 'alreadyProcessed',
NoErrorsFound = 'noErrorsFound',
InvalidDestDomain = 'invalidDestDomain',
UnknownDestChain = 'unknownDestChain',
RecipientNotContract = 'recipientNotContract',
RecipientNotHandler = 'recipientNotHandler',
IcaCallFailure = 'icaCallFailure',
@ -35,7 +33,6 @@ export interface LinkProperty {
export interface MessageDebugDetails {
status: MessageDebugStatus;
properties: Map<string, string | LinkProperty>;
details: string;
}

@ -1,11 +1,12 @@
import { constants } from 'ethers';
import { MultiProvider, hyperlaneEnvironments } from '@hyperlane-xyz/sdk';
import { ChainMap, MultiProvider } from '@hyperlane-xyz/sdk';
import { Message, MessageStatus } from '../../types';
import { logger } from '../../utils/logger';
import { toDecimalNumber } from '../../utils/number';
import { getChainEnvironment } from '../chains/utils';
import type { ChainConfig } from '../chains/chainConfig';
import { getContractAddress } from '../chains/utils';
import { debugExplorerMessage } from '../debugger/debugMessage';
import { MessageDebugStatus } from '../debugger/types';
import { TX_HASH_ZERO } from '../messages/placeholderMessages';
@ -24,13 +25,11 @@ const PROCESS_TOPIC_0 = '0x1cae38cdd3d3919489272725a5ae62a4f48b2989b0dae843d3c27
export async function fetchDeliveryStatus(
multiProvider: MultiProvider,
customChainConfigs: ChainMap<ChainConfig>,
message: Message,
): Promise<MessageDeliveryStatusResponse> {
const destName = multiProvider.getChainName(message.destinationChainId);
const destEnv = getChainEnvironment(multiProvider, destName);
// TODO PI support here
const destMailboxAddr = hyperlaneEnvironments[destEnv][destName]?.mailbox;
if (!destMailboxAddr) throw new Error(`No mailbox address found for dest ${destName}`);
const destMailboxAddr = getContractAddress(customChainConfigs, destName, 'mailbox');
const logs = await fetchMessageLogs(multiProvider, message, destMailboxAddr);
@ -42,7 +41,7 @@ export async function fetchDeliveryStatus(
message.destinationChainId,
log.transactionHash,
);
// If a delivery (aka process) tx is found, assume success
// If a delivery (aka process) tx is found, mark as success
const result: MessageDeliverySuccessResult = {
status: MessageStatus.Delivered,
deliveryTransaction: {
@ -65,7 +64,7 @@ export async function fetchDeliveryStatus(
};
return result;
} else {
const debugResult = await debugExplorerMessage(message, multiProvider);
const debugResult = await debugExplorerMessage(multiProvider, customChainConfigs, message);
if (
debugResult.status === MessageDebugStatus.NoErrorsFound ||
debugResult.status === MessageDebugStatus.AlreadyProcessed
@ -92,11 +91,7 @@ function fetchMessageLogs(multiProvider: MultiProvider, message: Message, mailbo
});
}
async function fetchTransactionDetails(
multiProvider: MultiProvider,
chainId: ChainId,
txHash: string,
) {
function fetchTransactionDetails(multiProvider: MultiProvider, chainId: ChainId, txHash: string) {
logger.debug(`Searching for transaction details for ${txHash}`);
const provider = multiProvider.getProvider(chainId);
return provider.getTransaction(txHash);

@ -2,17 +2,18 @@ import { useQuery } from '@tanstack/react-query';
import { useEffect, useMemo } from 'react';
import { toast } from 'react-toastify';
import { chainIdToMetadata } from '@hyperlane-xyz/sdk';
import { Message, MessageStatus } from '../../types';
import { logger } from '../../utils/logger';
import { MissingChainConfigToast } from '../chains/MissingChainConfigToast';
import { useChainConfigs } from '../chains/useChainConfigs';
import { useMultiProvider } from '../providers/multiProvider';
import { fetchDeliveryStatus } from './fetchDeliveryStatus';
export function useMessageDeliveryStatus({ message, pause }: { message: Message; pause: boolean }) {
const chainConfigs = useChainConfigs();
const multiProvider = useMultiProvider();
const serializedMessage = JSON.stringify(message);
const { data, error } = useQuery(
['messageDeliveryStatus', serializedMessage, pause],
@ -26,6 +27,7 @@ export function useMessageDeliveryStatus({ message, pause }: { message: Message;
domainId={message.originDomainId}
/>,
);
return null;
} else if (!multiProvider.tryGetChainMetadata(message.destinationChainId)) {
toast.error(
<MissingChainConfigToast
@ -33,18 +35,11 @@ export function useMessageDeliveryStatus({ message, pause }: { message: Message;
domainId={message.destinationDomainId}
/>,
);
}
// TODO enable PI support here
if (
message.isPiMsg ||
!chainIdToMetadata[message.originChainId] ||
!chainIdToMetadata[message.destinationChainId]
)
return null;
}
logger.debug('Fetching message delivery status for:', message.id);
const deliverStatus = await fetchDeliveryStatus(multiProvider, message);
const deliverStatus = await fetchDeliveryStatus(multiProvider, chainConfigs, message);
logger.debug('Message delivery status result', deliverStatus);
return deliverStatus;
},
@ -77,8 +72,6 @@ export function useMessageDeliveryStatus({ message, pause }: { message: Message;
{
status: data.debugStatus,
details: data.debugDetails,
originChainId: message.originChainId,
originTxHash: message.origin.hash,
},
];
}

@ -108,6 +108,7 @@ export function MessageDetails({ messageId, message: messageFromUrlParams }: Pro
status={status}
transaction={destination}
debugInfo={debugInfo}
isPiMsg={message.isPiMsg}
helpText={helpText.destination}
shouldBlur={shouldBlur}
/>

@ -20,6 +20,7 @@ interface TransactionCardProps {
status: MessageStatus;
transaction?: MessageTx;
debugInfo?: TransactionCardDebugInfo;
isPiMsg?: boolean;
helpText: string;
shouldBlur: boolean;
}
@ -27,8 +28,6 @@ interface TransactionCardProps {
export interface TransactionCardDebugInfo {
status: MessageDebugStatus;
details: string;
originChainId: ChainId;
originTxHash: string;
}
export function TransactionCard({
@ -37,6 +36,7 @@ export function TransactionCard({
status,
transaction,
debugInfo,
isPiMsg,
helpText,
shouldBlur,
}: TransactionCardProps) {
@ -109,7 +109,7 @@ export function TransactionCard({
{!transaction && status === MessageStatus.Failing && (
<div className="flex flex-col items-center py-5">
<div className="text-gray-700 text-center">
Destination delivery transaction currently failing
Delivery to destination chain is currently failing
</div>
{debugInfo && (
<>
@ -126,8 +126,13 @@ export function TransactionCard({
{!transaction && status === MessageStatus.Pending && (
<div className="flex flex-col items-center py-5">
<div className="text-gray-500 text-center max-w-xs">
Destination chain delivery transaction not yet found
Delivery to destination chain still in progress.
</div>
{isPiMsg && (
<div className="mt-2 text-gray-500 text-center text-sm max-w-xs">
Please ensure a relayer is running for this chain.
</div>
)}
<Spinner classes="mt-4 scale-75" />
</div>
)}

@ -25,7 +25,7 @@ export function usePiChainMessageSearchQuery({
endTimeFilter: number | null;
pause: boolean;
}) {
const { chainConfigs } = useChainConfigs();
const chainConfigs = useChainConfigs();
const multiProvider = useMultiProvider();
const { isLoading, isError, data } = useQuery(
[
@ -72,7 +72,7 @@ export function usePiChainMessageQuery({
messageId: string;
pause: boolean;
}) {
const { chainConfigs } = useChainConfigs();
const chainConfigs = useChainConfigs();
const multiProvider = useMultiProvider();
const { isLoading, isError, data } = useQuery(
['usePiChainMessageQuery', chainConfigs, messageId, pause],

@ -21,6 +21,7 @@ export default async function handler(
try {
const body = req.body as { chainId: ChainId };
if (!body.chainId) throw new Error('No chainId in body');
// TODO PI support here
if (!chainIdToMetadata[body.chainId]) throw new Error('ChainId is unsupported');
const multiProvider = new MultiProvider();
const nonce = await fetchLatestNonce(multiProvider, body.chainId);

@ -1,21 +0,0 @@
import type { NextPage } from 'next';
import { ContentFrame } from '../components/layout/ContentFrame';
import { TxDebugger } from '../features/debugger/TxDebugger';
const DebuggerPage: NextPage = () => {
return (
<ContentFrame>
<TxDebugger />
</ContentFrame>
);
};
// Required for dynamic routing to work by disabling Automatic Static Optimization
export function getServerSideProps() {
return {
props: {},
};
}
export default DebuggerPage;

@ -3,7 +3,6 @@ import { persist } from 'zustand/middleware';
import { ChainMap, MultiProvider } from '@hyperlane-xyz/sdk';
import { Environment } from './consts/environments';
import { ChainConfig } from './features/chains/chainConfig';
import { buildSmartProvider } from './features/providers/SmartMultiProvider';
import { logger } from './utils/logger';
@ -13,8 +12,6 @@ import { logger } from './utils/logger';
interface AppState {
chainConfigsV2: ChainMap<ChainConfig>; // v2 because schema changed
setChainConfigs: (configs: ChainMap<ChainConfig>) => void;
environment: Environment;
setEnvironment: (env: Environment) => void;
multiProvider: MultiProvider;
setMultiProvider: (mp: MultiProvider) => void;
bannerClassName: string;
@ -28,8 +25,6 @@ export const useStore = create<AppState>()(
setChainConfigs: (configs: ChainMap<ChainConfig>) => {
set(() => ({ chainConfigsV2: configs, multiProvider: buildSmartProvider(configs) }));
},
environment: Environment.Mainnet,
setEnvironment: (env: Environment) => set(() => ({ environment: env })),
multiProvider: new MultiProvider(),
setMultiProvider: (mp: MultiProvider) => set(() => ({ multiProvider: mp })),
bannerClassName: '',

@ -1,5 +1,4 @@
import { trimLeading0x } from './addresses';
import { logger } from './logger';
export function toTitleCase(str: string) {
return str.replace(/\w\S*/g, (txt) => {
@ -41,7 +40,6 @@ export function tryUtf8DecodeBytes(value: string, fatal = true) {
const decodedBody = decoder.decode(Buffer.from(trimLeading0x(value), 'hex'));
return decodedBody;
} catch (error) {
logger.debug('Unable to parse utf-8 bytes', value);
return undefined;
}
}

Loading…
Cancel
Save