Merge pull request #38 from hyperlane-xyz/pi-debug-support

- Add PI support for msg debugging and delivery status fetching
- Ensure chain config syncing only runs once per page
- Move MultiProvider into store to avoid frequent re-construction
- Remove old TxDebugger page and cleanup related code
- Improve error reason surfacing from SmartProvider
- Update widgets to 1.3.4 for timeline fix
pull/39/head
J M Rossy 2 years ago committed by GitHub
commit a3e4220169
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      package.json
  2. 46
      src/components/nav/EnvironmentSelector.tsx
  3. 8
      src/features/chains/ChainConfigSyncer.tsx
  4. 5
      src/features/chains/ConfigureChains.tsx
  5. 15
      src/features/chains/MissingChainConfigToast.tsx
  6. 1
      src/features/chains/chainConfig.ts
  7. 13
      src/features/chains/useChainConfigs.ts
  8. 31
      src/features/chains/utils.ts
  9. 175
      src/features/debugger/TxDebugger.tsx
  10. 326
      src/features/debugger/debugMessage.ts
  11. 2
      src/features/debugger/strings.ts
  12. 3
      src/features/debugger/types.ts
  13. 21
      src/features/deliveryStatus/fetchDeliveryStatus.ts
  14. 33
      src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  15. 1
      src/features/messages/MessageDetails.tsx
  16. 26
      src/features/messages/cards/TransactionCard.tsx
  17. 5
      src/features/messages/pi-queries/fetchPiChainMessages.test.ts
  18. 16
      src/features/messages/pi-queries/fetchPiChainMessages.ts
  19. 6
      src/features/messages/pi-queries/usePiChainMessageQuery.ts
  20. 35
      src/features/providers/SmartMultiProvider.ts
  21. 127
      src/features/providers/SmartProvider.ts
  22. 30
      src/features/providers/multiProvider.ts
  23. 29
      src/features/providers/types.ts
  24. 9
      src/pages/_app.tsx
  25. 1
      src/pages/api/latest-nonce.ts
  26. 21
      src/pages/debugger.tsx
  27. 30
      src/store.ts
  28. 2
      src/utils/string.ts
  29. 6
      src/utils/timeout.ts
  30. 22
      yarn.lock

@ -6,7 +6,7 @@
"dependencies": {
"@headlessui/react": "^1.7.11",
"@hyperlane-xyz/sdk": "1.3.4",
"@hyperlane-xyz/widgets": "1.3.3",
"@hyperlane-xyz/widgets": "1.3.4",
"@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6",
"@rainbow-me/rainbowkit": "^0.11.0",
"@tanstack/react-query": "^4.24.10",
@ -23,7 +23,7 @@
"urql": "^3.0.3",
"wagmi": "^0.11.6",
"zod": "^3.21.2",
"zustand": "^4.3.3"
"zustand": "^4.3.8"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.0.0",
@ -65,7 +65,6 @@
},
"types": "dist/src/index.d.ts",
"resolutions": {
"ethers": "^5.7",
"zustand": "^4.3.3"
"ethers": "^5.7"
}
}

@ -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) },
];

@ -0,0 +1,8 @@
import { PropsWithChildren } from 'react';
import { useQueryParamChainConfigSync } from './useChainConfigs';
export function ChainConfigSyncer({ children }: PropsWithChildren<Record<never, any>>) {
useQueryParamChainConfigSync();
return <>{children}</>;
}

@ -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..."
}
}

@ -1,9 +1,20 @@
import Link from 'next/link';
export function MissingChainConfigToast({ chainId }: { chainId: number }) {
export function MissingChainConfigToast({
chainId,
domainId,
}: {
chainId: number;
domainId: number;
}) {
const errorDesc = chainId
? `chain ID: ${chainId}`
: domainId
? `domain ID: ${domainId}`
: 'unknown message chain';
return (
<div>
<span>No chain config found for chain ID: {chainId}. </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,16 +16,21 @@ 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,
}));
}
// Use the chainConfigs from the store but with any
// chainConfigs from the query string merged in
export function useChainConfigsWithQueryParams() {
const { chainConfigs: storeConfigs, setChainConfigs } = 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 } = 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(
@ -369,7 +124,7 @@ async function debugMessageDelivery(
);
return { gasEstimate: deliveryGasEst.toString() };
} catch (err: any) {
logger.info('Estimate gas call failed');
logger.info('Estimate gas call failed:', err);
const errorReason = extractReasonString(err);
logger.debug(errorReason);
@ -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],
@ -20,21 +21,25 @@ export function useMessageDeliveryStatus({ message, pause }: { message: Message;
if (pause || !message || message.status === MessageStatus.Delivered) return null;
if (!multiProvider.tryGetChainMetadata(message.originChainId)) {
toast.error(<MissingChainConfigToast chainId={message.originChainId} />);
toast.error(
<MissingChainConfigToast
chainId={message.originChainId}
domainId={message.originDomainId}
/>,
);
return null;
} else if (!multiProvider.tryGetChainMetadata(message.destinationChainId)) {
toast.error(<MissingChainConfigToast chainId={message.destinationChainId} />);
}
// TODO enable PI support here
if (
message.isPiMsg ||
!chainIdToMetadata[message.originChainId] ||
!chainIdToMetadata[message.destinationChainId]
)
toast.error(
<MissingChainConfigToast
chainId={message.destinationChainId}
domainId={message.destinationDomainId}
/>,
);
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;
},
@ -67,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}
/>

@ -1,10 +1,7 @@
import Image from 'next/image';
import { Spinner } from '../../../components/animation/Spinner';
import { ChainLogo } from '../../../components/icons/ChainLogo';
import { HelpIcon } from '../../../components/icons/HelpIcon';
import { Card } from '../../../components/layout/Card';
import MailUnknown from '../../../images/icons/mail-unknown.svg';
import { MessageStatus, MessageTx } from '../../../types';
import { getDateTimeString, getHumanReadableTimeString } from '../../../utils/time';
import { getChainDisplayName } from '../../chains/utils';
@ -20,6 +17,7 @@ interface TransactionCardProps {
status: MessageStatus;
transaction?: MessageTx;
debugInfo?: TransactionCardDebugInfo;
isPiMsg?: boolean;
helpText: string;
shouldBlur: boolean;
}
@ -27,8 +25,6 @@ interface TransactionCardProps {
export interface TransactionCardDebugInfo {
status: MessageDebugStatus;
details: string;
originChainId: ChainId;
originTxHash: string;
}
export function TransactionCard({
@ -37,6 +33,7 @@ export function TransactionCard({
status,
transaction,
debugInfo,
isPiMsg,
helpText,
shouldBlur,
}: TransactionCardProps) {
@ -109,7 +106,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 && (
<>
@ -123,22 +120,19 @@ export function TransactionCard({
)}
</div>
)}
{!transaction && status === MessageStatus.Pending && (
{!transaction && (status === MessageStatus.Pending || status === MessageStatus.Unknown) && (
<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>
)}
{!transaction && status === MessageStatus.Unknown && (
<div className="flex flex-col items-center py-5">
<div className="text-gray-500 text-center max-w-xs">
Destination transaction tracking is unavailable for this message, sorry!{' '}
</div>
<Image src={MailUnknown} alt="" width={60} height={60} className="mt-7" />
</div>
)}
</Card>
);
}

@ -2,7 +2,7 @@ import { chainMetadata, hyperlaneEnvironments } from '@hyperlane-xyz/sdk';
import { Message, MessageStatus } from '../../../types';
import { ChainConfig } from '../../chains/chainConfig';
import { SmartMultiProvider } from '../../providers/multiProvider';
import { SmartMultiProvider } from '../../providers/SmartMultiProvider';
import { fetchMessagesFromPiChain } from './fetchPiChainMessages';
@ -56,6 +56,9 @@ const goerliMessage: Message = {
maxFeePerGas: 0,
maxPriorityPerGas: 0,
},
numPayments: 1,
totalGasAmount: '209736',
totalPayment: '1635940800000000',
isPiMsg: true,
};

@ -82,12 +82,18 @@ export async function fetchMessagesFromPiChain(
.map((l) => logToMessage(multiProvider, l, chainConfig))
.filter((m): m is Message => !!m);
const messagesWithGasPayments: Message[] = [];
// Avoiding parallelism here out of caution for RPC rate limits
for (const m of messages) {
messagesWithGasPayments.push(await tryFetchIgpGasPayments(m, chainConfig, multiProvider));
// Fetch IGP gas payments for each message if it's a small set
if (messages.length < 5) {
const messagesWithGasPayments: Message[] = [];
// Avoiding parallelism here out of caution for RPC rate limits
for (const m of messages) {
messagesWithGasPayments.push(await tryFetchIgpGasPayments(m, chainConfig, multiProvider));
}
return messagesWithGasPayments;
} else {
// Otherwise skip IGP gas fetching
return messages;
}
return messagesWithGasPayments;
}
async function fetchLogsForAddress(

@ -6,7 +6,7 @@ import { Message } from '../../../types';
import { ensureLeading0x } from '../../../utils/addresses';
import { logger } from '../../../utils/logger';
import { ChainConfig } from '../../chains/chainConfig';
import { useChainConfigsWithQueryParams } from '../../chains/useChainConfigs';
import { useChainConfigs } from '../../chains/useChainConfigs';
import { useMultiProvider } from '../../providers/multiProvider';
import { isValidSearchQuery } from '../queries/useMessageQuery';
@ -25,7 +25,7 @@ export function usePiChainMessageSearchQuery({
endTimeFilter: number | null;
pause: boolean;
}) {
const chainConfigs = useChainConfigsWithQueryParams();
const chainConfigs = useChainConfigs();
const multiProvider = useMultiProvider();
const { isLoading, isError, data } = useQuery(
[
@ -72,7 +72,7 @@ export function usePiChainMessageQuery({
messageId: string;
pause: boolean;
}) {
const chainConfigs = useChainConfigsWithQueryParams();
const chainConfigs = useChainConfigs();
const multiProvider = useMultiProvider();
const { isLoading, isError, data } = useQuery(
['usePiChainMessageQuery', chainConfigs, messageId, pause],

@ -0,0 +1,35 @@
import {
ChainMap,
ChainMetadata,
ChainName,
MultiProvider,
chainMetadata,
} from '@hyperlane-xyz/sdk';
import { logger } from '../../utils/logger';
import type { ChainConfig } from '../chains/chainConfig';
import { HyperlaneSmartProvider } from './SmartProvider';
export class SmartMultiProvider extends MultiProvider {
constructor(chainMetadata?: ChainMap<ChainMetadata>, options?: any) {
super(chainMetadata, options);
}
// Override to use SmartProvider instead of FallbackProvider
override tryGetProvider(chainNameOrId: ChainName | number): HyperlaneSmartProvider | null {
const metadata = this.tryGetChainMetadata(chainNameOrId);
if (!metadata) return null;
const { name, publicRpcUrls, blockExplorers } = metadata;
if (!this.providers[name] && (publicRpcUrls?.length || blockExplorers?.length)) {
this.providers[name] = new HyperlaneSmartProvider(metadata);
}
return (this.providers[name] as HyperlaneSmartProvider) || null;
}
}
export function buildSmartProvider(customChainConfigs: ChainMap<ChainConfig>) {
logger.debug('Building new SmartMultiProvider');
return new SmartMultiProvider({ ...chainMetadata, ...customChainConfigs });
}

@ -2,17 +2,20 @@ import { providers } from 'ethers';
import { ChainMetadata, ExplorerFamily } from '@hyperlane-xyz/sdk';
import { logAndThrow } from '../../utils/errors';
import { logger } from '../../utils/logger';
import { sleep } from '../../utils/timeout';
import { timeout } from '../../utils/timeout';
import { HyperlaneEtherscanProvider } from './HyperlaneEtherscanProvider';
import { HyperlaneJsonRpcProvider } from './HyperlaneJsonRpcProvider';
import { IProviderMethods, ProviderMethod } from './ProviderMethods';
import { ChainMetadataWithRpcConnectionInfo } from './types';
import {
ChainMetadataWithRpcConnectionInfo,
ProviderPerformResult,
ProviderStatus,
ProviderTimeoutResult,
} from './types';
const PROVIDER_STAGGER_DELAY_MS = 1000; // 1 seconds
const PROVIDER_TIMEOUT_MARKER = '__PROVIDER_TIMEOUT__';
type HyperlaneProvider = HyperlaneEtherscanProvider | HyperlaneJsonRpcProvider;
@ -83,48 +86,83 @@ export class HyperlaneSmartProvider extends providers.BaseProvider implements IP
let pIndex = 0;
const maxPIndex = supportedProviders.length - 1;
const providerResultPromises: Promise<any>[] = [];
const providerResultPromises: Promise<ProviderPerformResult>[] = [];
const providerResultErrors: unknown[] = [];
// TODO consider implementing quorum and/or retry logic here similar to FallbackProvider/RetryProvider
while (true) {
if (pIndex <= maxPIndex) {
// Trigger the next provider in line
const provider = supportedProviders[pIndex];
const providerUrl = provider.getBaseUrl();
const isLastProvider = pIndex === maxPIndex;
// Skip the explorer provider if it's currently in a cooldown period
if (
this.isExplorerProvider(provider) &&
provider.getQueryWaitTime() > 0 &&
pIndex < maxPIndex &&
!isLastProvider &&
method !== ProviderMethod.GetLogs // never skip GetLogs
) {
pIndex += 1;
continue;
}
const resultPromise = performWithLogging(provider, providerUrl, method, params, reqId);
providerResultPromises.push(resultPromise);
const timeoutPromise = sleep(PROVIDER_STAGGER_DELAY_MS, PROVIDER_TIMEOUT_MARKER);
const result = await Promise.any([resultPromise, timeoutPromise]);
const resultPromise = wrapProviderPerform(provider, providerUrl, method, params, reqId);
const timeoutPromise = timeout<ProviderTimeoutResult>(PROVIDER_STAGGER_DELAY_MS, {
status: ProviderStatus.Timeout,
});
const result = await Promise.race([resultPromise, timeoutPromise]);
if (result === PROVIDER_TIMEOUT_MARKER) {
if (result.status === ProviderStatus.Success) {
return result.value;
} else if (result.status === ProviderStatus.Timeout) {
logger.warn(
`Slow response from provider using ${providerUrl}. Triggering next provider if available`,
`Slow response from provider using ${providerUrl}.${
!isLastProvider ? ' Triggering next provider.' : ''
}`,
);
providerResultPromises.push(resultPromise);
pIndex += 1;
} else if (result.status === ProviderStatus.Error) {
logger.warn(
`Error from provider using ${providerUrl}.${
!isLastProvider ? ' Triggering next provider.' : ''
}`,
);
providerResultErrors.push(result.error);
pIndex += 1;
} else {
// Result looks good
return result;
throw new Error('Unexpected result from provider');
}
} else {
// All providers already triggered, wait for one to complete
const timeoutPromise = sleep(PROVIDER_STAGGER_DELAY_MS * 20, PROVIDER_TIMEOUT_MARKER);
const result = await Promise.any([...providerResultPromises, timeoutPromise]);
if (result === PROVIDER_TIMEOUT_MARKER) {
logAndThrow(`All providers failed or timed out for method ${method}`, result);
} else if (providerResultPromises.length > 0) {
// All providers already triggered, wait for one to complete or all to fail/timeout
const timeoutPromise = timeout<ProviderTimeoutResult>(PROVIDER_STAGGER_DELAY_MS * 20, {
status: ProviderStatus.Timeout,
});
const resultPromise = waitForProviderSuccess(providerResultPromises);
const result = await Promise.race([resultPromise, timeoutPromise]);
if (result.status === ProviderStatus.Success) {
return result.value;
} else if (result.status === ProviderStatus.Timeout) {
throwCombinedProviderErrors(
providerResultErrors,
`All providers timed out for method ${method}`,
);
} else if (result.status === ProviderStatus.Error) {
throwCombinedProviderErrors(
[result.error, ...providerResultErrors],
`All providers failed for method ${method}`,
);
} else {
return result;
throw new Error('Unexpected result from provider');
}
} else {
// All providers have already failed, all hope is lost
throwCombinedProviderErrors(
providerResultErrors,
`All providers failed for method ${method}`,
);
}
}
}
@ -134,20 +172,59 @@ export class HyperlaneSmartProvider extends providers.BaseProvider implements IP
}
}
function performWithLogging(
// Warp for additional logging and error handling
async function wrapProviderPerform(
provider: HyperlaneProvider,
providerUrl: string,
method: string,
params: any,
reqId: number,
): Promise<any> {
): Promise<ProviderPerformResult> {
try {
logger.debug(`Provider using ${providerUrl} performing method ${method} for reqId ${reqId}`);
return provider.perform(method, params, reqId);
const result = await provider.perform(method, params, reqId);
return { status: ProviderStatus.Success, value: result };
} catch (error) {
logger.error(`Error performing ${method} on provider ${providerUrl} for reqId ${reqId}`, error);
throw new Error(`Error performing ${method} with ${providerUrl} for reqId ${reqId}`);
return { status: ProviderStatus.Error, error };
}
}
async function waitForProviderSuccess(
_resultPromises: Promise<ProviderPerformResult>[],
): Promise<ProviderPerformResult> {
// A hack to remove the promise from the array when it resolves
const resolvedPromiseIndexes = new Set<number>();
const resultPromises = _resultPromises.map((p, i) =>
p.then((r) => {
resolvedPromiseIndexes.add(i);
return r;
}),
);
const combinedErrors: unknown[] = [];
for (let i = 0; i < resultPromises.length; i += 1) {
const promises = resultPromises.filter((_, i) => !resolvedPromiseIndexes.has(i));
const result = await Promise.race(promises);
if (result.status === ProviderStatus.Success) {
return result;
} else if (result.status === ProviderStatus.Error) {
combinedErrors.push(result.error);
} else {
return { status: ProviderStatus.Error, error: new Error('Unexpected result from provider') };
}
}
return {
status: ProviderStatus.Error,
// TODO combine errors
error: combinedErrors.length ? combinedErrors[0] : new Error('Unknown error from provider'),
};
}
function throwCombinedProviderErrors(errors: unknown[], fallbackMsg: string): void {
logger.error(fallbackMsg);
// TODO inspect the errors in some clever way to choose which to throw
if (errors.length > 0) throw errors[0];
else throw new Error(fallbackMsg);
}
function chainMetadataToProviderNetwork(chainMetadata: ChainMetadata): providers.Network {

@ -1,31 +1,5 @@
import { useMemo } from 'react';
import { ChainName, MultiProvider, chainMetadata } from '@hyperlane-xyz/sdk';
import { useChainConfigsWithQueryParams } from '../chains/useChainConfigs';
import { HyperlaneSmartProvider } from './SmartProvider';
export class SmartMultiProvider extends MultiProvider {
// Override to use SmartProvider instead of FallbackProvider
tryGetProvider(chainNameOrId: ChainName | number): HyperlaneSmartProvider | null {
const metadata = this.tryGetChainMetadata(chainNameOrId);
if (!metadata) return null;
const { name, publicRpcUrls, blockExplorers } = metadata;
if (!this.providers[name] && (publicRpcUrls?.length || blockExplorers?.length)) {
this.providers[name] = new HyperlaneSmartProvider(metadata);
}
return (this.providers[name] as HyperlaneSmartProvider) || null;
}
}
import { useStore } from '../../store';
export function useMultiProvider() {
const nameToConfig = useChainConfigsWithQueryParams();
const multiProvider = useMemo(
() => new SmartMultiProvider({ ...chainMetadata, ...nameToConfig }),
[nameToConfig],
);
return multiProvider;
return useStore((s) => s.multiProvider);
}

@ -9,3 +9,32 @@ export type RpcConfigWithConnectionInfo = ChainMetadata['publicRpcUrls'][number]
export interface ChainMetadataWithRpcConnectionInfo extends ChainMetadata {
publicRpcUrls: RpcConfigWithConnectionInfo[];
}
export enum ProviderStatus {
Success = 'success',
Error = 'error',
Timeout = 'timeout',
}
export interface ProviderPerformResultBase {
status: ProviderStatus;
}
export interface ProviderSuccessResult extends ProviderPerformResultBase {
status: ProviderStatus.Success;
value: any;
}
export interface ProviderErrorResult extends ProviderPerformResultBase {
status: ProviderStatus.Error;
error: unknown;
}
export interface ProviderTimeoutResult extends ProviderPerformResultBase {
status: ProviderStatus.Timeout;
}
export type ProviderPerformResult =
| ProviderSuccessResult
| ProviderErrorResult
| ProviderTimeoutResult;

@ -18,6 +18,7 @@ import '@hyperlane-xyz/widgets/styles.css';
import { ErrorBoundary } from '../components/errors/ErrorBoundary';
import { AppLayout } from '../components/layout/AppLayout';
import { config } from '../consts/config';
import { ChainConfigSyncer } from '../features/chains/ChainConfigSyncer';
import '../styles/fonts.css';
import '../styles/global.css';
import { useIsSsr } from '../utils/ssr';
@ -75,9 +76,11 @@ export default function App({ Component, router, pageProps }: AppProps) {
> */}
<QueryClientProvider client={reactQueryClient}>
<UrqlProvider value={urqlClient}>
<AppLayout pathName={router.pathname}>
<Component {...pageProps} />
</AppLayout>
<ChainConfigSyncer>
<AppLayout pathName={router.pathname}>
<Component {...pageProps} />
</AppLayout>
</ChainConfigSyncer>
</UrqlProvider>
</QueryClientProvider>
<ToastContainer transition={Zoom} position={toast.POSITION.BOTTOM_RIGHT} limit={2} />

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

@ -1,18 +1,19 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { ChainMap } from '@hyperlane-xyz/sdk';
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';
// Keeping everything here for now as state is simple
// Will refactor into slices as necessary
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;
setBanner: (className: string) => void;
}
@ -22,16 +23,29 @@ export const useStore = create<AppState>()(
(set) => ({
chainConfigsV2: {},
setChainConfigs: (configs: ChainMap<ChainConfig>) => {
set(() => ({ chainConfigsV2: configs }));
set({ chainConfigsV2: configs, multiProvider: buildSmartProvider(configs) });
},
multiProvider: buildSmartProvider({}),
setMultiProvider: (mp: MultiProvider) => {
set({ multiProvider: mp });
},
environment: Environment.Mainnet,
setEnvironment: (env: Environment) => set(() => ({ environment: env })),
bannerClassName: '',
setBanner: (className: string) => set(() => ({ bannerClassName: className })),
setBanner: (className: string) => set({ bannerClassName: className }),
}),
{
name: 'hyperlane', // name in storage
partialize: (state) => ({ chainConfigsV2: state.chainConfigsV2 }), // fields to persist
onRehydrateStorage: () => {
logger.debug('Rehydrating state');
return (state, error) => {
if (error || !state) {
logger.error('Error during hydration', error);
return;
}
state.setMultiProvider(buildSmartProvider(state.chainConfigsV2));
logger.debug('Hydration finished');
};
},
},
),
);

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

@ -40,10 +40,14 @@ export async function fetchWithTimeout(
return response;
}
export function sleep(milliseconds: number, resolveValue: any = true) {
export function timeout<T>(milliseconds: number, resolveValue: T): Promise<T> {
return new Promise((resolve) => setTimeout(() => resolve(resolveValue), milliseconds));
}
export function sleep(milliseconds: number): Promise<true> {
return timeout(milliseconds, true);
}
export const PROMISE_TIMEOUT = '__promise_timeout__';
export async function promiseTimeout<T>(promise: Promise<T>, milliseconds: number) {

@ -1311,7 +1311,7 @@ __metadata:
dependencies:
"@headlessui/react": ^1.7.11
"@hyperlane-xyz/sdk": 1.3.4
"@hyperlane-xyz/widgets": 1.3.3
"@hyperlane-xyz/widgets": 1.3.4
"@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6"
"@rainbow-me/rainbowkit": ^0.11.0
"@tanstack/react-query": ^4.24.10
@ -1345,7 +1345,7 @@ __metadata:
urql: ^3.0.3
wagmi: ^0.11.6
zod: ^3.21.2
zustand: ^4.3.3
zustand: ^4.3.8
languageName: unknown
linkType: soft
@ -1376,14 +1376,14 @@ __metadata:
languageName: node
linkType: hard
"@hyperlane-xyz/widgets@npm:1.3.3":
version: 1.3.3
resolution: "@hyperlane-xyz/widgets@npm:1.3.3"
"@hyperlane-xyz/widgets@npm:1.3.4":
version: 1.3.4
resolution: "@hyperlane-xyz/widgets@npm:1.3.4"
peerDependencies:
"@hyperlane-xyz/sdk": ^1.3.3
"@hyperlane-xyz/sdk": ^1.3.4
react: ^18
react-dom: ^18
checksum: 30558e8b5dfa768259151c0a675c219fce5bdd64c30d7cb1c4ea41bad1bc9ff951b920c3eb26548de56c87097b9d4fea7fa20f5b893a542834b595c4459bc280
checksum: 16a7447e9c0d767b2dabcb56f5450e3c376e1d6d58116228a052c7e1dd2442264dd7c2ac8bbd43ccd7c5601dd62ff664b58ec8e91cedbacfcff2573ce568d961
languageName: node
linkType: hard
@ -11153,9 +11153,9 @@ __metadata:
languageName: node
linkType: hard
"zustand@npm:^4.3.3":
version: 4.3.3
resolution: "zustand@npm:4.3.3"
"zustand@npm:^4.3.1, zustand@npm:^4.3.8":
version: 4.3.8
resolution: "zustand@npm:4.3.8"
dependencies:
use-sync-external-store: 1.2.0
peerDependencies:
@ -11166,6 +11166,6 @@ __metadata:
optional: true
react:
optional: true
checksum: fe0277ab0ce14849dea254cbdcfe83d4614364d38e616875f75ef2419f29085f5b03605efabeb33189a4557e647efaa294e0984c4f642e0566f445a7b207cdd6
checksum: 24db6bf063ce1fc8b2ee238f13211a88f43236541a716e5f6f706f613c671a45332465f9ed06d694f8c353da3d24c53ea668e5712a86aceda9ad74f6c433e8c0
languageName: node
linkType: hard

Loading…
Cancel
Save