@hyperlane-xyz/widgets
Version:
Common react components for Hyperlane projects
165 lines • 6.82 kB
JavaScript
import { useCallback, useState } from 'react';
import { fetchWithTimeout } from '@hyperlane-xyz/utils';
import { HYPERLANE_EXPLORER_API_URL } from '../consts.js';
import { widgetLogger } from '../logger.js';
import { queryExplorerForBlock } from '../utils/explorers.js';
import { useInterval } from '../utils/timeout.js';
import { MessageStatus, MessageStage as Stage, } from './types.js';
const logger = widgetLogger.child({ module: 'useMessageStage' });
const VALIDATION_TIME_EST = 5;
const DEFAULT_BLOCK_TIME_EST = 3;
const DEFAULT_FINALITY_BLOCKS = 3;
const defaultTiming = {
[Stage.Finalized]: null,
[Stage.Validated]: null,
[Stage.Relayed]: null,
};
export function useMessageStage({ message, multiProvider, explorerApiUrl = HYPERLANE_EXPLORER_API_URL, retryInterval = 2000, }) {
// Tempting to use react-query here as we did in Explorer but
// avoiding for now to keep dependencies for this lib minimal
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
const fetcher = useCallback(() => {
// Skip invalid or placeholder messages
if (!isValidMessage(message))
return;
// Don't re-run for failing messages
if (message.status === MessageStatus.Failing && data)
return;
// Don't re-run for pending, validated messages
if (message.status === MessageStatus.Pending &&
data?.stage === Stage.Validated)
return;
setIsLoading(true);
fetchMessageState(message, multiProvider, explorerApiUrl)
.then((result) => {
setData(result);
setError(null);
})
.catch((e) => setError(e.toString()))
.finally(() => setIsLoading(false));
}, [explorerApiUrl, multiProvider, message, data]);
useInterval(fetcher, retryInterval);
return {
stage: data?.stage
? data.stage
: isValidMessage(message)
? Stage.Sent
: Stage.Preparing,
timings: data?.timings ? data.timings : defaultTiming,
isLoading,
error,
};
}
async function fetchMessageState(message, multiProvider, explorerApiUrl) {
const { status, nonce, originDomainId, destinationDomainId, origin, destination, } = message;
const { blockNumber: originBlockNumber, timestamp: originTimestamp } = origin;
const destTimestamp = destination?.timestamp;
const relayEstimate = Math.floor((await getBlockTimeEst(destinationDomainId, multiProvider)) * 1.5);
const finalityBlocks = await getFinalityBlocks(originDomainId, multiProvider);
const finalityEstimate = finalityBlocks * (await getBlockTimeEst(originDomainId, multiProvider));
if (status === MessageStatus.Delivered && destTimestamp) {
// For delivered messages, just to rough estimates for stages
// This saves us from making extra explorer calls. May want to revisit in future
const totalDuration = Math.round((destTimestamp - originTimestamp) / 1000);
const finalityDuration = Math.max(Math.min(finalityEstimate, totalDuration - VALIDATION_TIME_EST), 1);
const remaining = totalDuration - finalityDuration;
const validateDuration = Math.max(Math.min(Math.round(remaining * 0.25), VALIDATION_TIME_EST), 1);
const relayDuration = Math.max(remaining - validateDuration, 1);
return {
stage: Stage.Relayed,
timings: {
[Stage.Finalized]: finalityDuration,
[Stage.Validated]: validateDuration,
[Stage.Relayed]: relayDuration,
},
};
}
const latestNonce = await tryFetchLatestNonce(originDomainId, multiProvider, explorerApiUrl);
if (latestNonce && latestNonce >= nonce) {
return {
stage: Stage.Validated,
timings: {
[Stage.Finalized]: finalityEstimate,
[Stage.Validated]: VALIDATION_TIME_EST,
[Stage.Relayed]: relayEstimate,
},
};
}
const latestBlock = await tryFetchChainLatestBlock(originDomainId, multiProvider);
const finalizedBlock = originBlockNumber + finalityBlocks;
if (latestBlock && parseInt(latestBlock.number.toString()) > finalizedBlock) {
return {
stage: Stage.Finalized,
timings: {
[Stage.Finalized]: finalityEstimate,
[Stage.Validated]: VALIDATION_TIME_EST,
[Stage.Relayed]: relayEstimate,
},
};
}
return {
stage: Stage.Sent,
timings: {
[Stage.Finalized]: finalityEstimate,
[Stage.Validated]: VALIDATION_TIME_EST,
[Stage.Relayed]: relayEstimate,
},
};
}
async function getFinalityBlocks(domainId, multiProvider) {
const metadata = await multiProvider.getChainMetadata(domainId);
if (metadata?.blocks?.confirmations)
return metadata.blocks.confirmations;
else
return DEFAULT_FINALITY_BLOCKS;
}
async function getBlockTimeEst(domainId, multiProvider) {
const metadata = await multiProvider.getChainMetadata(domainId);
return metadata?.blocks?.estimateBlockTime || DEFAULT_BLOCK_TIME_EST;
}
async function tryFetchChainLatestBlock(domainId, multiProvider) {
const metadata = multiProvider.tryGetChainMetadata(domainId);
if (!metadata)
return null;
logger.debug(`Attempting to fetch latest block for:`, metadata.name);
try {
const block = await queryExplorerForBlock(metadata.name, multiProvider, 'latest');
return block;
}
catch (error) {
logger.error('Error fetching latest block', error);
return null;
}
}
async function tryFetchLatestNonce(domainId, multiProvider, explorerApiUrl) {
const metadata = multiProvider.tryGetChainMetadata(domainId);
if (!metadata)
return null;
logger.debug(`Attempting to fetch nonce for:`, metadata.name);
try {
const response = await fetchWithTimeout(`${explorerApiUrl}/latest-nonce`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ chainId: metadata.chainId }),
}, 3000);
const result = await response.json();
logger.debug(`Found nonce:`, result.nonce);
return result.nonce;
}
catch (error) {
logger.error('Error fetching nonce', error);
return null;
}
}
function isValidMessage(message) {
return !!(message &&
message.originChainId &&
message.destinationChainId &&
message.originDomainId &&
message.destinationDomainId);
}
//# sourceMappingURL=useMessageStage.js.map