commit
07b53fe0da
@ -1,8 +1,9 @@ |
|||||||
{ |
{ |
||||||
"tabWidth": 2, |
"tabWidth": 2, |
||||||
|
"printWidth": 100, |
||||||
"singleQuote": true, |
"singleQuote": true, |
||||||
"trailingComma": "all", |
"trailingComma": "all", |
||||||
"importOrder": ["^@abacus-network/(.*)$", "^../(.*)$", "^./(.*)$"], |
"importOrder": ["^@hyperlane-xyz/(.*)$", "^../(.*)$", "^./(.*)$"], |
||||||
"importOrderSeparation": true, |
"importOrderSeparation": true, |
||||||
"importOrderSortSpecifiers": true |
"importOrderSortSpecifiers": true |
||||||
} |
} |
||||||
|
@ -0,0 +1,46 @@ |
|||||||
|
import Image from 'next/future/image'; |
||||||
|
import { ChangeEvent } from 'react'; |
||||||
|
|
||||||
|
import SearchIcon from '../../images/icons/search.svg'; |
||||||
|
import XIcon from '../../images/icons/x.svg'; |
||||||
|
import { Spinner } from '../animation/Spinner'; |
||||||
|
import { IconButton } from '../buttons/IconButton'; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
value: string; |
||||||
|
placeholder: string; |
||||||
|
onChangeValue: (v: string) => void; |
||||||
|
fetching: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
export function SearchBar({ value, placeholder, onChangeValue, fetching }: Props) { |
||||||
|
const onChange = (event: ChangeEvent<HTMLInputElement> | null) => { |
||||||
|
const value = event?.target?.value || ''; |
||||||
|
onChangeValue(value); |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="flex items-center bg-white w-full rounded shadow-md border border-blue-50"> |
||||||
|
<input |
||||||
|
value={value} |
||||||
|
onChange={onChange} |
||||||
|
type="text" |
||||||
|
placeholder={placeholder} |
||||||
|
className="p-2 sm:px-4 md:px-5 flex-1 h-10 sm:h-12 rounded focus:outline-none" |
||||||
|
/> |
||||||
|
<div className="bg-beige-300 h-10 sm:h-12 w-10 sm:w-12 flex items-center justify-center rounded"> |
||||||
|
{fetching && <Spinner classes="scale-[30%] mr-2.5" />} |
||||||
|
{!fetching && !value && <Image src={SearchIcon} width={20} height={20} />} |
||||||
|
{!fetching && value && ( |
||||||
|
<IconButton |
||||||
|
imgSrc={XIcon} |
||||||
|
title="Clear search" |
||||||
|
width={28} |
||||||
|
height={28} |
||||||
|
onClick={() => onChange(null)} |
||||||
|
/> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,99 @@ |
|||||||
|
import Image from 'next/future/image'; |
||||||
|
|
||||||
|
import BugIcon from '../../images/icons/bug.svg'; |
||||||
|
import ErrorIcon from '../../images/icons/error-circle.svg'; |
||||||
|
import SearchOffIcon from '../../images/icons/search-off.svg'; |
||||||
|
import ShrugIcon from '../../images/icons/shrug.svg'; |
||||||
|
import { Fade } from '../animation/Fade'; |
||||||
|
|
||||||
|
export function SearchError({ |
||||||
|
show, |
||||||
|
text, |
||||||
|
imgSrc, |
||||||
|
imgWidth, |
||||||
|
}: { |
||||||
|
show: boolean; |
||||||
|
text: string; |
||||||
|
imgSrc: any; |
||||||
|
imgWidth: number; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
// Absolute position for overlaying cross-fade
|
||||||
|
<div className="absolute left-0 right-0 top-10"> |
||||||
|
<Fade show={show}> |
||||||
|
<div className="flex justify-center my-10"> |
||||||
|
<div className="flex flex-col items-center justify-center max-w-md px-3 py-5"> |
||||||
|
<Image src={imgSrc} width={imgWidth} className="opacity-80" /> |
||||||
|
<div className="mt-4 text-center leading-loose text-gray-700">{text}</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Fade> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function NoSearchError({ show }: { show: boolean }) { |
||||||
|
return ( |
||||||
|
<SearchError |
||||||
|
show={show} |
||||||
|
imgSrc={BugIcon} |
||||||
|
text="Enter a transaction hash that involved at least one Hyperlane message to begin." |
||||||
|
imgWidth={50} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function SearchInvalidError({ |
||||||
|
show, |
||||||
|
allowAddress, |
||||||
|
}: { |
||||||
|
show: boolean; |
||||||
|
allowAddress: boolean; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<SearchError |
||||||
|
show={show} |
||||||
|
imgSrc={SearchOffIcon} |
||||||
|
text={`Sorry, that search input is not valid. Please try ${ |
||||||
|
allowAddress ? 'an account addresses or ' : '' |
||||||
|
}a transaction hash like 0xABC123...`}
|
||||||
|
imgWidth={70} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function SearchEmptyError({ |
||||||
|
show, |
||||||
|
hasInput, |
||||||
|
allowAddress, |
||||||
|
}: { |
||||||
|
show: boolean; |
||||||
|
hasInput: boolean; |
||||||
|
allowAddress: boolean; |
||||||
|
}) { |
||||||
|
return ( |
||||||
|
<SearchError |
||||||
|
show={show} |
||||||
|
imgSrc={ShrugIcon} |
||||||
|
text={`Sorry, no results found. Please try ${ |
||||||
|
hasInput |
||||||
|
? allowAddress |
||||||
|
? 'a different address or transaction hash' |
||||||
|
: 'a different transaction hash' |
||||||
|
: 'again later' |
||||||
|
}.`}
|
||||||
|
imgWidth={110} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function SearchUnknownError({ show }: { show: boolean }) { |
||||||
|
return ( |
||||||
|
<SearchError |
||||||
|
show={show} |
||||||
|
imgSrc={ErrorIcon} |
||||||
|
text="Sorry, an error has occurred. Please try a query or try again later." |
||||||
|
imgWidth={70} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
export enum Environment { |
||||||
|
Mainnet = 'mainnet', |
||||||
|
Testnet2 = 'testnet2', |
||||||
|
} |
||||||
|
|
||||||
|
export const envDisplayValue = { |
||||||
|
[Environment.Mainnet]: 'Mainnet', |
||||||
|
[Environment.Testnet2]: 'Testnet', |
||||||
|
}; |
@ -0,0 +1,151 @@ |
|||||||
|
import { useQuery } from '@tanstack/react-query'; |
||||||
|
import Image from 'next/future/image'; |
||||||
|
import { useState } from 'react'; |
||||||
|
|
||||||
|
import { Fade } from '../../components/animation/Fade'; |
||||||
|
import { CopyButton } from '../../components/buttons/CopyButton'; |
||||||
|
import { SearchBar } from '../../components/search/SearchBar'; |
||||||
|
import { |
||||||
|
NoSearchError, |
||||||
|
SearchEmptyError, |
||||||
|
SearchInvalidError, |
||||||
|
SearchUnknownError, |
||||||
|
} from '../../components/search/SearchError'; |
||||||
|
import { envDisplayValue } from '../../consts/environments'; |
||||||
|
import ShrugIcon from '../../images/icons/shrug.svg'; |
||||||
|
import { useStore } from '../../store'; |
||||||
|
import useDebounce from '../../utils/debounce'; |
||||||
|
import { sanitizeString, toTitleCase } from '../../utils/string'; |
||||||
|
import { isValidSearchQuery } from '../search/utils'; |
||||||
|
|
||||||
|
import { MessageDebugResult, TxDebugStatus, debugMessageForHash } from './debugMessage'; |
||||||
|
|
||||||
|
export function TxDebugger() { |
||||||
|
const environment = useStore((s) => s.environment); |
||||||
|
|
||||||
|
// Search text input
|
||||||
|
const [searchInput, setSearchInput] = useState(''); |
||||||
|
const debouncedSearchInput = useDebounce(searchInput, 750); |
||||||
|
const hasInput = !!debouncedSearchInput; |
||||||
|
const sanitizedInput = sanitizeString(debouncedSearchInput); |
||||||
|
const isValidInput = isValidSearchQuery(sanitizedInput, false); |
||||||
|
|
||||||
|
const { |
||||||
|
isLoading: fetching, |
||||||
|
isError: hasError, |
||||||
|
data, |
||||||
|
} = useQuery( |
||||||
|
['debugMessage', isValidInput, sanitizedInput, environment], |
||||||
|
() => { |
||||||
|
if (!isValidInput || !sanitizedInput) return null; |
||||||
|
else return debugMessageForHash(sanitizedInput, environment); |
||||||
|
}, |
||||||
|
{ retry: false }, |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<SearchBar |
||||||
|
value={searchInput} |
||||||
|
onChangeValue={setSearchInput} |
||||||
|
fetching={fetching} |
||||||
|
placeholder="Search transaction hash to debug message" |
||||||
|
/> |
||||||
|
<div className="w-full h-[38.05rem] mt-5 bg-white shadow-md border border-blue-50 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 (${envDisplayValue[environment]})`}</h2> |
||||||
|
</div> |
||||||
|
|
||||||
|
<Fade show={isValidInput && !hasError && !!data}> |
||||||
|
<div className="px-2 sm:px-4 md:px-5"> |
||||||
|
<DebugResult result={data} /> |
||||||
|
</div> |
||||||
|
</Fade> |
||||||
|
<SearchEmptyError |
||||||
|
show={isValidInput && !hasError && !fetching && !data} |
||||||
|
hasInput={hasInput} |
||||||
|
allowAddress={false} |
||||||
|
/> |
||||||
|
<NoSearchError show={!hasInput && !hasError} /> |
||||||
|
<SearchInvalidError show={hasInput && !hasError && !isValidInput} allowAddress={false} /> |
||||||
|
<SearchUnknownError show={hasInput && hasError} /> |
||||||
|
</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" /> |
||||||
|
<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" /> |
||||||
|
<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">{m.summary}</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> |
||||||
|
<div className="relative ml-2 truncate max-w-xs sm:max-w-sm md:max-w-lg"> |
||||||
|
{val} |
||||||
|
</div> |
||||||
|
{val.length > 20 && ( |
||||||
|
<CopyButton copyValue={val} width={12} height={12} classes="ml-2" /> |
||||||
|
)} |
||||||
|
</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> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,247 @@ |
|||||||
|
// Based on debug script in monorepo
|
||||||
|
// https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/infra/scripts/debug-message.ts
|
||||||
|
import { IMessageRecipient__factory } from '@hyperlane-xyz/core'; |
||||||
|
import { |
||||||
|
ChainName, |
||||||
|
DispatchedMessage, |
||||||
|
DomainIdToChainName, |
||||||
|
HyperlaneCore, |
||||||
|
MultiProvider, |
||||||
|
chainConnectionConfigs, |
||||||
|
} from '@hyperlane-xyz/sdk'; |
||||||
|
import { utils } from '@hyperlane-xyz/utils'; |
||||||
|
|
||||||
|
import { Environment } from '../../consts/environments'; |
||||||
|
import { errorToString } from '../../utils/errors'; |
||||||
|
import { logger } from '../../utils/logger'; |
||||||
|
import { chunk } from '../../utils/string'; |
||||||
|
|
||||||
|
export enum TxDebugStatus { |
||||||
|
NotFound = 'notFound', |
||||||
|
NoMessages = 'noMessages', |
||||||
|
MessagesFound = 'messagesFound', |
||||||
|
} |
||||||
|
|
||||||
|
export enum MessageDebugStatus { |
||||||
|
NoErrorsFound = 'noErrorsFound', |
||||||
|
InvalidDestDomain = 'invalidDestDomain', |
||||||
|
UnknownDestChain = 'unknownDestChain', |
||||||
|
RecipientNotContract = 'RecipientNotContract', |
||||||
|
HandleCallFailure = 'handleCallFailure', |
||||||
|
} |
||||||
|
|
||||||
|
export interface DebugNotFoundResult { |
||||||
|
status: TxDebugStatus.NotFound; |
||||||
|
details: string; |
||||||
|
} |
||||||
|
|
||||||
|
export interface DebugNoMessagesResult { |
||||||
|
status: TxDebugStatus.NoMessages; |
||||||
|
chainName: string; |
||||||
|
details: string; |
||||||
|
explorerLink?: string; |
||||||
|
} |
||||||
|
|
||||||
|
interface MessageDetails { |
||||||
|
status: MessageDebugStatus; |
||||||
|
properties: Map<string, string>; |
||||||
|
summary: string; |
||||||
|
} |
||||||
|
|
||||||
|
export interface DebugMessagesFoundResult { |
||||||
|
status: TxDebugStatus.MessagesFound; |
||||||
|
chainName: string; |
||||||
|
explorerLink?: string; |
||||||
|
messageDetails: MessageDetails[]; |
||||||
|
} |
||||||
|
|
||||||
|
export type MessageDebugResult = |
||||||
|
| DebugNotFoundResult |
||||||
|
| DebugNoMessagesResult |
||||||
|
| DebugMessagesFoundResult; |
||||||
|
|
||||||
|
export async function debugMessageForHash( |
||||||
|
txHash: string, |
||||||
|
environment: Environment, |
||||||
|
): Promise<MessageDebugResult> { |
||||||
|
// TODO use RPC with api keys
|
||||||
|
const multiProvider = new MultiProvider(chainConnectionConfigs); |
||||||
|
|
||||||
|
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 { transactionReceipt, chainName, explorerLink } = txDetails; |
||||||
|
const core = HyperlaneCore.fromEnvironment(environment, multiProvider); |
||||||
|
const dispatchedMessages = core.getDispatchedMessages(transactionReceipt); |
||||||
|
|
||||||
|
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: MessageDetails[] = []; |
||||||
|
for (let i = 0; i < dispatchedMessages.length; i++) { |
||||||
|
logger.debug(`Checking message ${i + 1} of ${dispatchedMessages.length}`); |
||||||
|
messageDetails.push(await checkMessage(core, multiProvider, dispatchedMessages[i])); |
||||||
|
logger.debug(`Done checking message ${i + 1}`); |
||||||
|
} |
||||||
|
return { |
||||||
|
status: TxDebugStatus.MessagesFound, |
||||||
|
chainName, |
||||||
|
explorerLink, |
||||||
|
messageDetails, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
async function findTransactionDetails(txHash: string, multiProvider: MultiProvider) { |
||||||
|
const chains = multiProvider.chains().filter((n) => !n.startsWith('test')); |
||||||
|
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, blockExplorerUrl } = multiProvider.getChainConnection(chainName); |
||||||
|
// TODO explorer may be faster, more robust way to get tx and its logs
|
||||||
|
// Note: receipt is null if tx not found
|
||||||
|
const transactionReceipt = await provider.getTransactionReceipt(txHash); |
||||||
|
if (transactionReceipt) { |
||||||
|
logger.info('Tx found', txHash, chainName); |
||||||
|
// TODO use getTxExplorerLink here, must reconcile wagmi consts and sdk consts
|
||||||
|
const explorerLink = blockExplorerUrl ? `${blockExplorerUrl}/tx/${txHash}` : undefined; |
||||||
|
return { transactionReceipt, chainName, explorerLink }; |
||||||
|
} else { |
||||||
|
logger.debug('Tx not found', txHash, chainName); |
||||||
|
throw new Error(`Tx not found on ${chainName}`); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function checkMessage( |
||||||
|
core: HyperlaneCore<any>, |
||||||
|
multiProvider: MultiProvider<any>, |
||||||
|
message: DispatchedMessage, |
||||||
|
) { |
||||||
|
logger.debug(JSON.stringify(message)); |
||||||
|
const properties = new Map<string, string>(); |
||||||
|
properties.set('Sender', message.parsed.sender.toString()); |
||||||
|
properties.set('Recipient', message.parsed.sender.toString()); |
||||||
|
properties.set('Origin Domain', message.parsed.origin.toString()); |
||||||
|
properties.set('Destination Domain', message.parsed.destination.toString()); |
||||||
|
properties.set('Leaf index', message.leafIndex.toString()); |
||||||
|
properties.set('Raw Bytes', message.message); |
||||||
|
|
||||||
|
const destinationChain = DomainIdToChainName[message.parsed.destination]; |
||||||
|
|
||||||
|
if (!destinationChain) { |
||||||
|
logger.info(`Unknown destination domain ${message.parsed.destination}`); |
||||||
|
return { |
||||||
|
status: MessageDebugStatus.InvalidDestDomain, |
||||||
|
properties, |
||||||
|
summary: |
||||||
|
'The destination domain id is invalid. Note, domain ids usually do not match chain ids. See https://docs.hyperlane.xyz/hyperlane-docs/developers/domains', |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
logger.debug(`Destination chain: ${destinationChain}`); |
||||||
|
|
||||||
|
if (!core.knownChain(destinationChain)) { |
||||||
|
logger.info(`Destination chain ${destinationChain} unknown for environment`); |
||||||
|
return { |
||||||
|
status: MessageDebugStatus.UnknownDestChain, |
||||||
|
properties, |
||||||
|
summary: `Destination chain ${destinationChain} is not included in this message's environment. See https://docs.hyperlane.xyz/hyperlane-docs/developers/domains`, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
const destinationInbox = core.getMailboxPair( |
||||||
|
DomainIdToChainName[message.parsed.origin], |
||||||
|
destinationChain, |
||||||
|
).destinationInbox; |
||||||
|
|
||||||
|
const messageHash = utils.messageHash(message.message, message.leafIndex); |
||||||
|
logger.debug(`Message hash: ${messageHash}`); |
||||||
|
|
||||||
|
const processed = await destinationInbox.messages(messageHash); |
||||||
|
if (processed === 1) { |
||||||
|
logger.info('Message has already been processed'); |
||||||
|
// TODO: look for past events to find the exact tx in which the message was processed.
|
||||||
|
return { |
||||||
|
status: MessageDebugStatus.NoErrorsFound, |
||||||
|
properties, |
||||||
|
summary: 'No errors found, this message has already been processed.', |
||||||
|
}; |
||||||
|
} else { |
||||||
|
logger.debug('Message not yet processed'); |
||||||
|
} |
||||||
|
|
||||||
|
const recipientAddress = utils.bytes32ToAddress(message.parsed.recipient); |
||||||
|
const recipientIsContract = await isContract(multiProvider, destinationChain, recipientAddress); |
||||||
|
|
||||||
|
if (!recipientIsContract) { |
||||||
|
logger.info(`Recipient address ${recipientAddress} is not a contract`); |
||||||
|
return { |
||||||
|
status: MessageDebugStatus.RecipientNotContract, |
||||||
|
properties, |
||||||
|
summary: `Recipient address ${recipientAddress} is not a contract. Ensure bytes32 value is not malformed.`, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
const destinationProvider = multiProvider.getChainProvider(destinationChain); |
||||||
|
const recipient = IMessageRecipient__factory.connect(recipientAddress, destinationProvider); |
||||||
|
|
||||||
|
try { |
||||||
|
await recipient.estimateGas.handle( |
||||||
|
message.parsed.origin, |
||||||
|
message.parsed.sender, |
||||||
|
message.parsed.body, |
||||||
|
{ from: destinationInbox.address }, |
||||||
|
); |
||||||
|
logger.debug('Calling recipient `handle` function from the inbox does not revert'); |
||||||
|
return { |
||||||
|
status: MessageDebugStatus.NoErrorsFound, |
||||||
|
properties, |
||||||
|
summary: 'No errors found, this message appears to be deliverable.', |
||||||
|
}; |
||||||
|
} catch (err: any) { |
||||||
|
logger.info(`Error calling recipient handle function from the inbox`); |
||||||
|
const errorString = errorToString(err); |
||||||
|
logger.debug(errorString); |
||||||
|
return { |
||||||
|
status: MessageDebugStatus.HandleCallFailure, |
||||||
|
properties, |
||||||
|
// TODO format the error string better to be easier to understand
|
||||||
|
summary: `Error calling handle on the recipient contract. Details: ${errorString}`, |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function isContract(multiProvider: MultiProvider<any>, chain: ChainName, address: string) { |
||||||
|
const provider = multiProvider.getChainProvider(chain); |
||||||
|
const code = await provider.getCode(address); |
||||||
|
// "Empty" code
|
||||||
|
return code && code !== '0x'; |
||||||
|
} |
After Width: | Height: | Size: 696 B |
@ -0,0 +1,14 @@ |
|||||||
|
import type { NextPage } from 'next'; |
||||||
|
|
||||||
|
import { ContentFrame } from '../components/layout/ContentFrame'; |
||||||
|
import { TxDebugger } from '../features/debugger/TxDebugger'; |
||||||
|
|
||||||
|
const Debugger: NextPage = () => { |
||||||
|
return ( |
||||||
|
<ContentFrame> |
||||||
|
<TxDebugger /> |
||||||
|
</ContentFrame> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default Debugger; |
@ -0,0 +1,19 @@ |
|||||||
|
import create from 'zustand'; |
||||||
|
|
||||||
|
import { Environment } from './consts/environments'; |
||||||
|
|
||||||
|
// Keeping everything here for now as state is simple
|
||||||
|
// Will refactor into slices as necessary
|
||||||
|
interface AppState { |
||||||
|
environment: Environment; |
||||||
|
setEnvironment: (env: Environment) => void; |
||||||
|
bannerClassName: string; |
||||||
|
setBanner: (env: string) => void; |
||||||
|
} |
||||||
|
|
||||||
|
export const useStore = create<AppState>()((set) => ({ |
||||||
|
environment: Environment.Mainnet, |
||||||
|
setEnvironment: (env: Environment) => set(() => ({ environment: env })), |
||||||
|
bannerClassName: '', |
||||||
|
setBanner: (className: string) => set(() => ({ bannerClassName: className })), |
||||||
|
})); |
@ -0,0 +1,10 @@ |
|||||||
|
import { trimToLength } from './string'; |
||||||
|
|
||||||
|
export function errorToString(error: any, maxLength = 300) { |
||||||
|
if (!error) return 'Unknown Error'; |
||||||
|
if (typeof error === 'string') return trimToLength(error, maxLength); |
||||||
|
if (typeof error === 'number') return `Error code: ${error}`; |
||||||
|
const details = error.message || error.reason || error; |
||||||
|
if (typeof details === 'string') return trimToLength(details, maxLength); |
||||||
|
return trimToLength(JSON.stringify(details), maxLength); |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
import { chainIdToChain } from '../consts/networksConfig'; |
||||||
|
|
||||||
|
export function getTxExplorerLink(chainId: number, hash?: string) { |
||||||
|
if (!chainId || !hash) return null; |
||||||
|
|
||||||
|
const chain = chainIdToChain[chainId]; |
||||||
|
if (!chain?.blockExplorers) return null; |
||||||
|
|
||||||
|
if (chain.blockExplorers.etherscan) { |
||||||
|
return `${chain.blockExplorers.etherscan.url}/tx/${hash}`; |
||||||
|
} |
||||||
|
if (chain.blockExplorers.default) { |
||||||
|
return `${chain.blockExplorers.default.url}/tx/${hash}`; |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
@ -1,5 +1,3 @@ |
|||||||
export function invertKeysAndValues(data: any) { |
export function invertKeysAndValues(data: any) { |
||||||
return Object.fromEntries( |
return Object.fromEntries(Object.entries(data).map(([key, value]) => [value, key])); |
||||||
Object.entries(data).map(([key, value]) => [value, key]), |
|
||||||
); |
|
||||||
} |
} |
||||||
|
Loading…
Reference in new issue