Improve staggered trigger algo for SmartProvider

Fix race condition bug in HyperlaneEtherscanProvider
Use SmartProvider throughout explorer
Fetch gas details for PI messages
Upgrade SDK and Widgets libs
pull/35/head
J M Rossy 2 years ago
parent a20e21eaec
commit 81c3505e8a
  1. 4
      package.json
  2. 1
      src/features/debugger/debugMessage.ts
  3. 55
      src/features/deliveryStatus/fetchDeliveryStatus.ts
  4. 2
      src/features/messages/MessageDetails.tsx
  5. 2
      src/features/messages/cards/GasDetailsCard.tsx
  6. 4
      src/features/messages/cards/TimelineCard.tsx
  7. 85
      src/features/messages/pi-queries/fetchPiChainMessages.test.ts
  8. 260
      src/features/messages/pi-queries/fetchPiChainMessages.ts
  9. 74
      src/features/providers/HyperlaneEtherscanProvider.ts
  10. 40
      src/features/providers/HyperlaneJsonRpcProvider.ts
  11. 47
      src/features/providers/SmartProvider.test.ts
  12. 72
      src/features/providers/SmartProvider.ts
  13. 35
      src/features/providers/multiProvider.ts
  14. 6
      src/utils/errors.ts
  15. 2
      src/utils/explorers.ts
  16. 44
      yarn.lock

@ -5,8 +5,8 @@
"author": "J M Rossy",
"dependencies": {
"@headlessui/react": "^1.7.11",
"@hyperlane-xyz/sdk": "1.3.2",
"@hyperlane-xyz/widgets": "1.3.2",
"@hyperlane-xyz/sdk": "1.3.3",
"@hyperlane-xyz/widgets": "1.3.3",
"@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6",
"@rainbow-me/rainbowkit": "^0.11.0",
"@tanstack/react-query": "^4.24.10",

@ -435,7 +435,6 @@ async function tryCheckIgpGasFunded(
let gasAlreadyFunded = BigNumber.from(0);
if (totalGasAmount) {
const filter = igp.filters.GasPayment(messageId, null, null);
// TODO restrict blocks here to avoid rpc errors
const matchedEvents = (await igp.queryFilter(filter)) || [];
logger.debug(`Found ${matchedEvents.length} payments to IGP for msg ${messageId}`);
logger.debug(matchedEvents);

@ -3,7 +3,6 @@ import { constants } from 'ethers';
import { MultiProvider, hyperlaneEnvironments } from '@hyperlane-xyz/sdk';
import { Message, MessageStatus } from '../../types';
import { queryExplorerForLogs, queryExplorerForTx } from '../../utils/explorers';
import { logger } from '../../utils/logger';
import { toDecimalNumber } from '../../utils/number';
import { getChainEnvironment } from '../chains/utils';
@ -33,12 +32,12 @@ export async function fetchDeliveryStatus(
const destMailboxAddr = hyperlaneEnvironments[destEnv][destName]?.mailbox;
if (!destMailboxAddr) throw new Error(`No mailbox address found for dest ${destName}`);
const logs = await fetchExplorerLogsForMessage(multiProvider, message, destMailboxAddr);
const logs = await fetchMessageLogs(multiProvider, message, destMailboxAddr);
if (logs?.length) {
logger.debug(`Found delivery log for tx ${message.origin.hash}`);
const log = logs[0]; // Should only be 1 log per message delivery
const txDetails = await tryFetchTransactionDetails(
const txDetails = await fetchTransactionDetails(
multiProvider,
message.destinationChainId,
log.transactionHash,
@ -47,21 +46,21 @@ export async function fetchDeliveryStatus(
const result: MessageDeliverySuccessResult = {
status: MessageStatus.Delivered,
deliveryTransaction: {
timestamp: toDecimalNumber(log.timeStamp) * 1000,
timestamp: toDecimalNumber(txDetails.timestamp || 0) * 1000,
hash: log.transactionHash,
from: txDetails?.from || constants.AddressZero,
to: txDetails?.to || constants.AddressZero,
blockHash: txDetails?.blockHash || TX_HASH_ZERO,
from: txDetails.from || constants.AddressZero,
to: txDetails.to || constants.AddressZero,
blockHash: txDetails.blockHash || TX_HASH_ZERO,
blockNumber: toDecimalNumber(log.blockNumber),
mailbox: constants.AddressZero,
nonce: txDetails?.nonce || 0,
gasLimit: toDecimalNumber(txDetails?.gasLimit || 0),
gasPrice: toDecimalNumber(txDetails?.gasPrice || 0),
effectiveGasPrice: toDecimalNumber(txDetails?.gasPrice || 0),
gasUsed: toDecimalNumber(log.gasUsed),
cumulativeGasUsed: toDecimalNumber(log.gasUsed),
maxFeePerGas: toDecimalNumber(txDetails?.maxFeePerGas || 0),
maxPriorityPerGas: toDecimalNumber(txDetails?.maxPriorityFeePerGas || 0),
nonce: txDetails.nonce || 0,
gasLimit: toDecimalNumber(txDetails.gasLimit || 0),
gasPrice: toDecimalNumber(txDetails.gasPrice || 0),
effectiveGasPrice: toDecimalNumber(txDetails.gasPrice || 0),
gasUsed: toDecimalNumber(txDetails.gasLimit || 0),
cumulativeGasUsed: toDecimalNumber(txDetails.gasLimit || 0),
maxFeePerGas: toDecimalNumber(txDetails.maxFeePerGas || 0),
maxPriorityPerGas: toDecimalNumber(txDetails.maxPriorityFeePerGas || 0),
},
};
return result;
@ -83,28 +82,22 @@ export async function fetchDeliveryStatus(
}
}
function fetchExplorerLogsForMessage(
multiProvider: MultiProvider,
message: Message,
mailboxAddr: Address,
) {
function fetchMessageLogs(multiProvider: MultiProvider, message: Message, mailboxAddr: Address) {
const { msgId, origin, destinationChainId } = message;
logger.debug(`Searching for delivery logs for tx ${origin.hash}`);
const logsQueryPath = `module=logs&action=getLogs&fromBlock=1&toBlock=latest&topic0=${PROCESS_TOPIC_0}&topic0_1_opr=and&topic1=${msgId}&address=${mailboxAddr}`;
return queryExplorerForLogs(multiProvider, destinationChainId, logsQueryPath);
const provider = multiProvider.getProvider(destinationChainId);
return provider.getLogs({
topics: [PROCESS_TOPIC_0, msgId],
address: mailboxAddr,
});
}
async function tryFetchTransactionDetails(
async function fetchTransactionDetails(
multiProvider: MultiProvider,
chainId: ChainId,
txHash: string,
) {
try {
const tx = await queryExplorerForTx(multiProvider, chainId, txHash);
return tx;
} catch (error) {
// Swallowing error if there's an issue so we can still surface delivery confirmation
logger.error('Failed to fetch tx details', txHash, chainId);
return null;
}
logger.debug(`Searching for transaction details for ${txHash}`);
const provider = multiProvider.getProvider(chainId);
return provider.getTransaction(txHash);
}

@ -113,7 +113,7 @@ export function MessageDetails({ messageId, message: messageFromUrlParams }: Pro
/>
{!message.isPiMsg && <TimelineCard message={message} shouldBlur={shouldBlur} />}
<ContentDetailsCard message={message} shouldBlur={shouldBlur} />
{!message.isPiMsg && <GasDetailsCard message={message} shouldBlur={shouldBlur} />}
<GasDetailsCard message={message} shouldBlur={shouldBlur} />
{isIcaMsg && <IcaDetailsCard message={message} shouldBlur={shouldBlur} />}
</div>
</>

@ -100,7 +100,9 @@ export function GasDetailsCard({ message, shouldBlur }: Props) {
function computeAvgGasPrice(unit: string, gasAmount?: string, payment?: string) {
try {
if (!gasAmount || !payment) return null;
const gasBN = new BigNumber(gasAmount);
const paymentBN = new BigNumber(payment);
if (gasBN.isZero() || paymentBN.isZero()) return null;
const wei = paymentBN.div(gasAmount).toFixed(0);
const formatted = utils.formatUnits(wei, unit).toString();
return { wei, formatted };

@ -8,7 +8,7 @@ interface Props {
shouldBlur?: boolean;
}
export function TimelineCard({ message }: Props) {
export function TimelineCard({ message, shouldBlur }: Props) {
const { stage, timings } = useMessageStage({ message });
return (
@ -17,7 +17,7 @@ export function TimelineCard({ message }: Props) {
<h3 className="text-gray-500 font-medium text-md mr-2">Delivery Timeline</h3>
<HelpIcon size={16} text="A breakdown of the stages for delivering a message" />
</div> */}
<div className="-mx-2 sm:mx-0 -my-2">
<div className={`-mx-2 sm:mx-0 -my-2 ${shouldBlur && 'blur-xs'}`}>
<MessageTimeline status={message.status} stage={stage} timings={timings} />
</div>
</Card>

@ -1,17 +1,17 @@
import { MultiProvider, chainMetadata, hyperlaneEnvironments } from '@hyperlane-xyz/sdk';
import { chainMetadata, hyperlaneEnvironments } from '@hyperlane-xyz/sdk';
import { Message, MessageStatus } from '../../../types';
import { ChainConfig } from '../../chains/chainConfig';
import { SmartMultiProvider } from '../../providers/multiProvider';
import { fetchMessagesFromPiChain } from './fetchPiChainMessages';
// NOTE: THESE TESTS WILL NO LONGER WORK ONCE THE MESSAGE USED BELOW
// IS OUT OF PROVIDER_LOGS_BLOCK_WINDOW USED TO QUERY
// THESE WERE MOSTLY USED FOR TDD OF THE FETCHING CODE
// TODO: MOCK THE PROVIDER + EXPLORER TO MAKE THESE NETWORK INDEPENDENT
// NOTE: THE GOERLI MESSAGE MAY NEED TO BE UPDATED ON OCCASION AS IT GETS TOO OLD
// THIS IS DUE TO LIMITATIONS OF THE RPC PROVIDER
// TODO: MOCK THE PROVIDER TO MAKE THESE NETWORK INDEPENDENT
jest.setTimeout(30000);
const multiProvider = new MultiProvider();
const goerliMailbox = hyperlaneEnvironments.testnet.goerli.mailbox;
const goerliConfigWithExplorer: ChainConfig = {
...chainMetadata.goerli,
@ -20,41 +20,50 @@ const goerliConfigWithExplorer: ChainConfig = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { blockExplorers, ...goerliConfigNoExplorer } = goerliConfigWithExplorer;
// https://explorer.hyperlane.xyz/message/0x328b582541b896dbb2258750bb26ac9d3b6d24424cf5b62ba466f949e80f0f48
const txHash = '0x051695a31a6feccacebf09f0c426e21ff4d5c894603faa658c3b4cff89653978';
const msgId = '0x328b582541b896dbb2258750bb26ac9d3b6d24424cf5b62ba466f949e80f0f48';
const senderAddress = '0x0637a1360ea44602dae5c4ba515c2bcb6c762fbc';
const recipientAddress = '0xa76a3e719e5ff7159a29b8876272052b89b3589f';
// https://explorer.hyperlane.xyz/message/0xfec74152c40d8dfe117bf1a83ba443c85d0de8962272445019c526686a70459e
const txHash = '0xea0ba6b69ca70147d7cfdc2a806fe6b6ca5bce143408ebcf348fdec30cdd7daf';
const msgId = '0xfec74152c40d8dfe117bf1a83ba443c85d0de8962272445019c526686a70459e';
const senderAddress = '0x405bfdecb33230b4ad93c29ba4499b776cfba189';
const recipientAddress = '0x5da3b8d6f73df6003a490072106730218c475aad';
const goerliMessage = {
const goerliMessage: Message = {
id: '',
msgId: '0x328b582541b896dbb2258750bb26ac9d3b6d24424cf5b62ba466f949e80f0f48',
msgId: '0xfec74152c40d8dfe117bf1a83ba443c85d0de8962272445019c526686a70459e',
originChainId: 5,
originDomainId: 5,
destinationChainId: 421613,
destinationDomainId: 421613,
nonce: 21048,
destinationChainId: 43113,
destinationDomainId: 43113,
nonce: 25459,
body: '0x48656c6c6f21',
originTimestamp: 1678444980000,
originTransaction: {
blockNumber: 8629961,
sender: '0x405BFdEcB33230b4Ad93C29ba4499b776CfBa189',
recipient: '0x5da3b8d6F73dF6003A490072106730218c475AAd',
status: MessageStatus.Unknown,
origin: {
timestamp: 1682842440000,
hash: '0xea0ba6b69ca70147d7cfdc2a806fe6b6ca5bce143408ebcf348fdec30cdd7daf',
from: '0x06C8798aA665bDbeea6aBa6fC1b1d9bbDCa8d613',
to: '0x405BFdEcB33230b4Ad93C29ba4499b776CfBa189',
blockHash: '0x62ac4553144fedd8582bc8d5c4e5186d8885f92d85aafd48a6bb4f3cf077e0e9',
blockNumber: 8916552,
mailbox: '0xCC737a94FecaeC165AbCf12dED095BB13F037685',
nonce: 0,
gasLimit: 0,
gasPrice: 0,
effectiveGasPrice: 0,
gasUsed: 0,
timestamp: 1678444980000,
transactionHash: '0x051695a31a6feccacebf09f0c426e21ff4d5c894603faa658c3b4cff89653978',
cumulativeGasUsed: 0,
maxFeePerGas: 0,
maxPriorityPerGas: 0,
},
sender: '0x0637A1360Ea44602DAe5c4ba515c2BCb6C762fbc',
recipient: '0xa76A3E719E5ff7159a29B8876272052b89B3589F',
status: 'unknown',
isPiMsg: true,
};
describe.skip('fetchMessagesFromPiChain', () => {
describe('fetchMessagesFromPiChain', () => {
it('Fetches messages using explorer for tx hash', async () => {
const messages = await fetchMessagesFromPiChain(
goerliConfigWithExplorer,
{ input: txHash },
multiProvider,
createMP(goerliConfigWithExplorer),
);
expect(messages).toEqual([goerliMessage]);
});
@ -62,7 +71,7 @@ describe.skip('fetchMessagesFromPiChain', () => {
const messages = await fetchMessagesFromPiChain(
goerliConfigWithExplorer,
{ input: msgId },
multiProvider,
createMP(goerliConfigWithExplorer),
);
expect(messages).toEqual([goerliMessage]);
});
@ -71,9 +80,9 @@ describe.skip('fetchMessagesFromPiChain', () => {
goerliConfigWithExplorer,
{
input: senderAddress,
fromBlock: goerliMessage.originTransaction.blockNumber - 100,
fromBlock: goerliMessage.origin.blockNumber - 100,
},
multiProvider,
createMP(goerliConfigWithExplorer),
);
const testMsg = messages.find((m) => m.msgId === msgId);
expect(testMsg).toBeTruthy();
@ -83,9 +92,9 @@ describe.skip('fetchMessagesFromPiChain', () => {
goerliConfigWithExplorer,
{
input: recipientAddress,
fromBlock: goerliMessage.originTransaction.blockNumber - 100,
fromBlock: goerliMessage.origin.blockNumber - 100,
},
multiProvider,
createMP(goerliConfigWithExplorer),
);
const testMsg = messages.find((m) => m.msgId === msgId);
expect(testMsg).toBeTruthy();
@ -94,7 +103,7 @@ describe.skip('fetchMessagesFromPiChain', () => {
const messages = await fetchMessagesFromPiChain(
goerliConfigNoExplorer,
{ input: txHash },
multiProvider,
createMP(goerliConfigNoExplorer),
);
expect(messages).toEqual([goerliMessage]);
});
@ -102,7 +111,7 @@ describe.skip('fetchMessagesFromPiChain', () => {
const messages = await fetchMessagesFromPiChain(
goerliConfigNoExplorer,
{ input: msgId },
multiProvider,
createMP(goerliConfigNoExplorer),
);
expect(messages).toEqual([goerliMessage]);
});
@ -112,7 +121,7 @@ describe.skip('fetchMessagesFromPiChain', () => {
{
input: senderAddress,
},
multiProvider,
createMP(goerliConfigNoExplorer),
);
const testMsg = messages.find((m) => m.msgId === msgId);
expect(testMsg).toBeTruthy();
@ -123,7 +132,7 @@ describe.skip('fetchMessagesFromPiChain', () => {
{
input: recipientAddress,
},
multiProvider,
createMP(goerliConfigNoExplorer),
);
const testMsg = messages.find((m) => m.msgId === msgId);
expect(testMsg).toBeTruthy();
@ -134,8 +143,12 @@ describe.skip('fetchMessagesFromPiChain', () => {
{
input: 'invalidInput',
},
multiProvider,
createMP(goerliConfigNoExplorer),
);
expect(messages).toEqual([]);
});
});
function createMP(config: ChainConfig) {
return new SmartMultiProvider({ ...chainMetadata, goerli: config });
}

@ -1,23 +1,14 @@
import { BigNumber, constants, ethers, providers } from 'ethers';
import { Mailbox__factory } from '@hyperlane-xyz/core';
import { IInterchainGasPaymaster__factory, Mailbox__factory } from '@hyperlane-xyz/core';
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { utils } from '@hyperlane-xyz/utils';
import { ExtendedLog, Message, MessageStatus } from '../../../types';
import { isValidAddress, isValidTransactionHash, normalizeAddress } from '../../../utils/addresses';
import {
queryExplorerForBlock,
queryExplorerForLogs,
queryExplorerForTxReceipt,
toProviderLog,
} from '../../../utils/explorers';
import { logger } from '../../../utils/logger';
import { ChainConfig } from '../../chains/chainConfig';
const PROVIDER_LOGS_BLOCK_WINDOW = 100_000;
const PROVIDER_BLOCK_DETAILS_WINDOW = 5_000;
const mailbox = Mailbox__factory.createInterface();
const dispatchTopic0 = mailbox.getEventTopic('Dispatch');
const dispatchIdTopic0 = mailbox.getEventTopic('DispatchId');
@ -26,8 +17,8 @@ const dispatchIdTopic0 = mailbox.getEventTopic('DispatchId');
export interface PiMessageQuery {
input: string;
fromBlock?: string | number;
toBlock?: string | number;
fromBlock?: providers.BlockTag;
toBlock?: providers.BlockTag;
}
export enum PiQueryType {
@ -69,19 +60,18 @@ export async function fetchMessagesFromPiChain(
multiProvider: MultiProvider,
queryType?: PiQueryType, // optionally force search down to just one type
): Promise<Message[]> {
const useExplorer = !!chainConfig.blockExplorers?.[0]?.apiUrl;
const input = query.input;
let logs: ExtendedLog[] = [];
if (isValidAddress(input) && (!queryType || queryType === PiQueryType.Address)) {
logs = await fetchLogsForAddress(chainConfig, query, multiProvider, useExplorer);
logs = await fetchLogsForAddress(chainConfig, query, multiProvider);
} else if (isValidTransactionHash(input)) {
if (!queryType || queryType === PiQueryType.TxHash) {
logs = await fetchLogsForTxHash(chainConfig, query, multiProvider, useExplorer);
logs = await fetchLogsForTxHash(chainConfig, query, multiProvider);
}
// Input may be a msg id, check that next
if ((!queryType || queryType === PiQueryType.MsgId) && !logs.length) {
logs = await fetchLogsForMsgId(chainConfig, query, multiProvider, useExplorer);
logs = await fetchLogsForMsgId(chainConfig, query, multiProvider);
}
} else {
logger.warn('Invalid PI search input', input, queryType);
@ -95,9 +85,7 @@ export async function fetchMessagesFromPiChain(
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, useExplorer),
);
messagesWithGasPayments.push(await tryFetchIgpGasPayments(m, chainConfig, multiProvider));
}
return messagesWithGasPayments;
}
@ -106,154 +94,83 @@ async function fetchLogsForAddress(
{ chainId, contracts }: ChainConfig,
query: PiMessageQuery,
multiProvider: MultiProvider,
useExplorer?: boolean,
): Promise<ExtendedLog[]> {
const address = query.input;
logger.debug(`Fetching logs for address ${address} on chain ${chainId}`);
const mailboxAddr = contracts.mailbox;
const dispatchTopic = utils.addressToBytes32(address);
if (useExplorer) {
return fetchLogsFromExplorer(
[
`&topic0=${dispatchTopic0}&topic0_1_opr=and&topic1=${dispatchTopic}&topic1_3_opr=or&topic3=${dispatchTopic}`,
// `&topic0=${processTopic0}&topic0_1_opr=and&topic1=${dispatchTopic}&topic1_3_opr=or&topic3=${dispatchTopic}`,
],
mailboxAddr,
chainId,
query,
multiProvider,
);
} else {
return fetchLogsFromProvider(
[
[dispatchTopic0, dispatchTopic],
[dispatchTopic0, null, null, dispatchTopic],
// [processTopic0, dispatchTopic],
// [processTopic0, null, null, dispatchTopic],
],
mailboxAddr,
chainId,
query,
multiProvider,
);
}
return fetchLogsFromProvider(
[
[dispatchTopic0, dispatchTopic],
[dispatchTopic0, null, null, dispatchTopic],
// [processTopic0, dispatchTopic],
// [processTopic0, null, null, dispatchTopic],
],
mailboxAddr,
chainId,
query,
multiProvider,
);
// }
}
async function fetchLogsForTxHash(
{ chainId }: ChainConfig,
query: PiMessageQuery,
multiProvider: MultiProvider,
useExplorer: boolean,
): Promise<ExtendedLog[]> {
const txHash = query.input;
logger.debug(`Fetching logs for txHash ${txHash} on chain ${chainId}`);
if (useExplorer) {
try {
const txReceipt = await queryExplorerForTxReceipt(multiProvider, chainId, txHash, false);
logger.debug(`Tx receipt found from explorer for chain ${chainId}`);
const block = await queryExplorerForBlock(
multiProvider,
chainId,
txReceipt.blockNumber,
false,
);
return txReceipt.logs.map((l) => ({
...l,
timestamp: parseBlockTimestamp(block),
from: txReceipt.from,
to: txReceipt.to,
}));
} catch (error) {
logger.debug(`Tx hash not found in explorer for chain ${chainId}`);
}
const provider = multiProvider.getProvider(chainId);
const txReceipt = await provider.getTransactionReceipt(txHash);
if (txReceipt) {
logger.debug(`Tx receipt found from provider for chain ${chainId}`);
const block = await tryFetchBlockFromProvider(provider, txReceipt.blockNumber);
return txReceipt.logs.map((l) => ({
...l,
timestamp: parseBlockTimestamp(block),
from: txReceipt.from,
to: txReceipt.to,
}));
} else {
const provider = multiProvider.getProvider(chainId);
const txReceipt = await provider.getTransactionReceipt(txHash);
if (txReceipt) {
logger.debug(`Tx receipt found from provider for chain ${chainId}`);
const block = await tryFetchBlockFromProvider(provider, txReceipt.blockNumber);
return txReceipt.logs.map((l) => ({
...l,
timestamp: parseBlockTimestamp(block),
from: txReceipt.from,
to: txReceipt.to,
}));
} else {
logger.debug(`Tx hash not found from provider for chain ${chainId}`);
}
logger.debug(`Tx hash not found from provider for chain ${chainId}`);
return [];
}
return [];
}
async function fetchLogsForMsgId(
chainConfig: ChainConfig,
query: PiMessageQuery,
multiProvider: MultiProvider,
useExplorer: boolean,
): Promise<ExtendedLog[]> {
const { contracts, chainId } = chainConfig;
const msgId = query.input;
logger.debug(`Fetching logs for msgId ${msgId} on chain ${chainId}`);
const mailboxAddr = contracts.mailbox;
const topic1 = msgId;
let logs: ExtendedLog[];
if (useExplorer) {
logs = await fetchLogsFromExplorer(
[
`&topic0=${dispatchIdTopic0}&topic0_1_opr=and&topic1=${topic1}`,
// `&topic0=${processIdTopic0}&topic0_1_opr=and&topic1=${topic1}`,
],
mailboxAddr,
chainId,
query,
multiProvider,
);
} else {
logs = await fetchLogsFromProvider(
[
[dispatchIdTopic0, topic1],
// [processIdTopic0, topic1],
],
mailboxAddr,
chainId,
query,
multiProvider,
);
}
const logs: ExtendedLog[] = await fetchLogsFromProvider(
[
[dispatchIdTopic0, topic1],
// [processIdTopic0, topic1],
],
mailboxAddr,
chainId,
query,
multiProvider,
);
// Grab first tx hash found in any log and get all logs for that tx
// Necessary because DispatchId/ProcessId logs don't contain useful info
if (logs.length) {
const txHash = logs[0].transactionHash;
logger.debug('Found tx hash with log with msg id. Hash:', txHash);
return (
fetchLogsForTxHash(chainConfig, { ...query, input: txHash }, multiProvider, useExplorer) || []
);
return fetchLogsForTxHash(chainConfig, { ...query, input: txHash }, multiProvider) || [];
}
return [];
}
async function fetchLogsFromExplorer(
paths: Array<string>,
contractAddr: Address,
chainId: ChainId,
query: PiMessageQuery,
multiProvider: MultiProvider,
): Promise<ExtendedLog[]> {
const fromBlock = query.fromBlock || '1';
const toBlock = query.toBlock || 'latest';
const base = `module=logs&action=getLogs&fromBlock=${fromBlock}&toBlock=${toBlock}&address=${contractAddr}`;
let logs: ExtendedLog[] = [];
for (const path of paths) {
// Originally use parallel requests here with Promise.all but immediately hit rate limit errors
const result = await queryExplorerForLogs(multiProvider, chainId, `${base}${path}`, false);
logs = [...logs, ...result.map(toProviderLog)];
}
return logs;
}
async function fetchLogsFromProvider(
topics: Array<Array<string | null>>,
contractAddr: Address,
@ -262,47 +179,37 @@ async function fetchLogsFromProvider(
multiProvider: MultiProvider,
): Promise<ExtendedLog[]> {
const provider = multiProvider.getProvider(chainId);
const latestBlock = await provider.getBlockNumber();
const fromBlock = query.fromBlock || latestBlock - PROVIDER_LOGS_BLOCK_WINDOW;
const toBlock = query.toBlock || 'latest';
// TODO may need chunking here to avoid RPC errors
const logs = (
await Promise.all(
topics.map((t) =>
provider.getLogs({
fromBlock,
toBlock,
address: contractAddr,
topics: t,
}),
),
)
).flat();
const timestamps: Record<number, number | undefined> = {};
const logsWithTimestamp = await Promise.all<ExtendedLog>(
logs.map(async (l) => {
const blockNum = l.blockNumber;
if (!timestamps[blockNum]) {
const block = await tryFetchBlockFromProvider(provider, blockNum, latestBlock);
timestamps[blockNum] = parseBlockTimestamp(block);
}
return {
...l,
timestamp: timestamps[blockNum],
};
}),
);
let logs: providers.Log[] = [];
for (const t of topics) {
logs = logs.concat(
await provider.getLogs({
fromBlock: query.fromBlock || 0,
toBlock: query.toBlock || 'latest',
address: contractAddr,
topics: t,
}),
);
}
// Too many logs to also fetch timestamps
if (logs.length > 10) return logs;
const logsWithTimestamp: ExtendedLog[] = [];
const timestamps: Record<number, number | null> = {};
for (const l of logs) {
const blockNum = l.blockNumber;
if (timestamps[blockNum] === undefined) {
const block = await tryFetchBlockFromProvider(provider, blockNum);
timestamps[blockNum] = parseBlockTimestamp(block) ?? null;
}
logsWithTimestamp.push({ ...l, timestamp: timestamps[blockNum] ?? undefined });
}
return logsWithTimestamp;
}
async function tryFetchBlockFromProvider(
provider: providers.Provider,
blockNum: number,
latestBlock?: number,
) {
async function tryFetchBlockFromProvider(provider: providers.Provider, blockNum: number) {
try {
if (latestBlock && latestBlock - blockNum > PROVIDER_BLOCK_DETAILS_WINDOW) return null;
logger.debug('Fetching block details for blockNum:', blockNum);
const block = await provider.getBlock(blockNum);
return block;
@ -312,7 +219,7 @@ async function tryFetchBlockFromProvider(
}
}
function parseBlockTimestamp(block: providers.Block | null): number | undefined {
function parseBlockTimestamp(block: providers.Block | null | undefined): number | undefined {
if (!block) return undefined;
return BigNumber.from(block.timestamp).toNumber() * 1000;
}
@ -382,10 +289,7 @@ function logToMessage(
async function tryFetchIgpGasPayments(
message: Message,
chainConfig: ChainConfig,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_multiProvider: MultiProvider,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_useExplorer?: boolean,
multiProvider: MultiProvider,
): Promise<Message> {
const { chainId, contracts } = chainConfig;
const igpAddr = contracts.interchainGasPaymaster;
@ -394,9 +298,21 @@ async function tryFetchIgpGasPayments(
return message;
}
// TODO implement gas payment fetching
// Mimic logic in debugger's tryCheckIgpGasFunded
// Either duplicate or refactor into shared util built on SmartProvider
const igp = IInterchainGasPaymaster__factory.connect(igpAddr, multiProvider.getProvider(chainId));
const filter = igp.filters.GasPayment(message.msgId);
const matchedEvents = (await igp.queryFilter(filter)) || [];
logger.debug(`Found ${matchedEvents.length} payments to IGP for msg ${message.msgId}`);
let totalGasAmount = BigNumber.from(0);
let totalPayment = BigNumber.from(0);
for (const payment of matchedEvents) {
totalGasAmount = totalGasAmount.add(payment.args.gasAmount);
totalPayment = totalPayment.add(payment.args.payment);
}
return message;
return {
...message,
totalGasAmount: totalGasAmount.toString(),
totalPayment: totalPayment.toString(),
numPayments: matchedEvents.length,
};
}

@ -11,7 +11,7 @@ type ExplorerConfig = Exclude<ChainMetadata['blockExplorers'], undefined>[number
// Used for crude rate-limiting of explorer queries without API keys
const hostToLastQueried: Record<string, number> = {};
const ETHERSCAN_THROTTLE_TIME = 5200; // 5.2 seconds
const ETHERSCAN_THROTTLE_TIME = 6000; // 6.0 seconds
export class HyperlaneEtherscanProvider
extends providers.EtherscanProvider
@ -25,6 +25,11 @@ export class HyperlaneEtherscanProvider
]);
constructor(public readonly explorerConfig: ExplorerConfig, network: providers.Network) {
if (!explorerConfig.apiKey) {
logger.warn(
'HyperlaneEtherscanProviders created without an API key will be severely rate limited. Consider using an API key for better reliability.',
);
}
super(network, explorerConfig.apiKey);
}
@ -52,27 +57,66 @@ export class HyperlaneEtherscanProvider
return new URL(this.getBaseUrl()).hostname;
}
getQueryWaitTime(): number {
if (!this.isCommunityResource()) return 0;
const hostname = this.getHostname();
const lastExplorerQuery = hostToLastQueried[hostname] || 0;
return ETHERSCAN_THROTTLE_TIME - (Date.now() - lastExplorerQuery);
}
async fetch(module: string, params: Record<string, any>, post?: boolean): Promise<any> {
if (!this.isCommunityResource()) return super.fetch(module, params, post);
const hostname = this.getHostname();
try {
const lastExplorerQuery = hostToLastQueried[hostname] || 0;
const waitTime = ETHERSCAN_THROTTLE_TIME - (Date.now() - lastExplorerQuery);
if (waitTime > 0) {
logger.debug(`HyperlaneEtherscanProvider waiting ${waitTime}ms to avoid rate limit`);
await sleep(waitTime);
}
const result = await super.fetch(module, params, post);
return result;
} finally {
hostToLastQueried[hostname] = Date.now();
let waitTime = this.getQueryWaitTime();
while (waitTime > 0) {
logger.debug(`HyperlaneEtherscanProvider waiting ${waitTime}ms to avoid rate limit`);
await sleep(waitTime);
waitTime = this.getQueryWaitTime();
}
hostToLastQueried[hostname] = Date.now();
return super.fetch(module, params, post);
}
async perform(method: string, params: any): Promise<any> {
logger.debug('HyperlaneEtherscanProvider performing method:', method);
async perform(method: string, params: any, reqId?: number): Promise<any> {
logger.debug(`HyperlaneEtherscanProvider performing method ${method} for reqId ${reqId}`);
if (!this.supportedMethods.includes(method as ProviderMethod))
throw new Error(`Unsupported method ${method}`);
return super.perform(method, params);
if (method === ProviderMethod.GetLogs) {
return this.performGetLogs(params);
} else {
return super.perform(method, params);
}
}
// Overriding to allow more than one topic value
async performGetLogs(params: { filter: providers.Filter }) {
const args: Record<string, any> = { action: 'getLogs' };
if (params.filter.fromBlock) args.fromBlock = checkLogTag(params.filter.fromBlock);
if (params.filter.toBlock) args.toBlock = checkLogTag(params.filter.toBlock);
if (params.filter.address) args.address = params.filter.address;
const topics = params.filter.topics;
if (topics?.length) {
if (topics.length > 2) throw new Error(`Unsupported topic count ${topics.length} (max 2)`);
for (let i = 0; i < topics.length; i++) {
const topic = topics[i];
if (!topic || typeof topic !== 'string' || topic.length !== 66)
throw new Error(`Unsupported topic format: ${topic}`);
args[`topic${i}`] = topic;
if (i < topics.length - 1) args[`topic${i}_${i + 1}_opr`] = 'and';
}
}
return this.fetch('logs', args);
}
}
// From ethers/providers/src.ts/providers/etherscan-provider.ts
function checkLogTag(blockTag: providers.BlockTag): number | 'latest' {
if (typeof blockTag === 'number') return blockTag;
if (blockTag === 'pending') throw new Error('pending not supported');
if (blockTag === 'latest') return blockTag;
return parseInt(blockTag.substring(2), 16);
}

@ -21,8 +21,8 @@ export class HyperlaneJsonRpcProvider
super(rpcConfig.connection ?? rpcConfig.http, network);
}
async perform(method: string, params: any): Promise<any> {
logger.debug('HyperlaneJsonRpcProvider performing method:', method);
async perform(method: string, params: any, reqId?: number): Promise<any> {
logger.debug(`HyperlaneJsonRpcProvider performing method ${method} for reqId ${reqId}`);
if (method === ProviderMethod.GetLogs) {
return this.performGetLogs(params);
} else {
@ -31,16 +31,16 @@ export class HyperlaneJsonRpcProvider
}
async performGetLogs(params: { filter: providers.Filter }) {
const deferToSuper = () => super.perform(ProviderMethod.GetLogs, params);
const superPerform = () => super.perform(ProviderMethod.GetLogs, params);
const paginationOptions = this.rpcConfig.pagination;
if (!paginationOptions || !params.filter) return deferToSuper();
if (!paginationOptions || !params.filter) return superPerform();
const { fromBlock, toBlock, address, topics } = params.filter;
// TODO update when sdk is updated
const { blocks: maxBlockRange, from: minBlockNumber } = paginationOptions;
if (!maxBlockRange && isNullish(minBlockNumber)) return deferToSuper();
if (!maxBlockRange && isNullish(minBlockNumber)) return superPerform();
const currentBlockNumber = await super.perform(ProviderMethod.GetBlockNumber, null);
@ -50,30 +50,32 @@ export class HyperlaneJsonRpcProvider
} else if (isBigNumberish(toBlock)) {
endBlock = BigNumber.from(toBlock).toNumber();
} else {
return deferToSuper();
return superPerform();
}
const minQueryable = maxBlockRange
? endBlock - maxBlockRange * NUM_LOG_BLOCK_RANGES_TO_QUERY + 1
: 0;
let startBlock: number;
if (fromBlock === 'earliest') {
if (isNullish(fromBlock) || fromBlock === 'earliest') {
startBlock = 0;
} else if (isBigNumberish(fromBlock)) {
startBlock = BigNumber.from(fromBlock).toNumber();
} else if (isNullish(fromBlock)) {
startBlock = Math.max(minQueryable, minBlockNumber ?? 0);
} else {
return deferToSuper();
return superPerform();
}
if (startBlock >= endBlock)
throw new Error(`Invalid range ${startBlock} - ${endBlock}: start >= end`);
if (minBlockNumber && startBlock < minBlockNumber)
throw new Error(`Invalid start ${startBlock}: below rpc minBlockNumber ${minBlockNumber}`);
if (startBlock > endBlock) {
logger.warn(`Start block ${startBlock} greater than end block. Using ${endBlock} instead`);
startBlock = endBlock;
}
const minQueryable = maxBlockRange
? endBlock - maxBlockRange * NUM_LOG_BLOCK_RANGES_TO_QUERY + 1
: 0;
if (startBlock < minQueryable) {
throw new Error(`Invalid range ${startBlock} - ${endBlock}: requires too many queries`);
logger.warn(`Start block ${startBlock} requires too many queries, using ${minQueryable}.`);
startBlock = minQueryable;
}
if (startBlock < minBlockNumber) {
logger.warn(`Start block ${startBlock} below config min, increasing to ${minBlockNumber}`);
startBlock = minBlockNumber;
}
const blockChunkRange = maxBlockRange || endBlock - startBlock;

@ -8,7 +8,7 @@ import { logger } from '../../utils/logger';
import { ProviderMethod } from './ProviderMethods';
import { HyperlaneSmartProvider } from './SmartProvider';
jest.setTimeout(40000);
jest.setTimeout(60_000);
const MIN_BLOCK_NUM = 8900000;
const DEFAULT_ACCOUNT = '0x9d525E28Fe5830eE92d7Aa799c4D21590567B595';
@ -130,23 +130,15 @@ describe('SmartProvider', () => {
expect(result2.length).toBeGreaterThan(10);
expect(areAddressesEqual(result2[0].address, WETH_CONTRACT)).toBeTruthy();
try {
logger.debug('Testing logs with too large from/to range');
const result3 = await provider.getLogs({
address: WETH_CONTRACT,
topics: [WETH_TRANSFER_TOPIC0],
fromBlock: MIN_BLOCK_NUM,
toBlock: 'latest',
});
if (config === justRpcsConfig) {
expect(false).toBe('Should throw error about minQueryable');
} else {
expect(result3.length).toBeGreaterThan(10);
expect(areAddressesEqual(result3[0].address, WETH_CONTRACT)).toBeTruthy();
}
} catch (error: any) {
expect(error.message).toMatch(/(.*)too many queries(.*)/);
}
logger.debug('Testing logs with large from/to range');
const result3 = await provider.getLogs({
address: WETH_CONTRACT,
topics: [WETH_TRANSFER_TOPIC0],
fromBlock: MIN_BLOCK_NUM,
toBlock: 'latest',
});
expect(result3.length).toBeGreaterThan(10);
expect(areAddressesEqual(result3[0].address, WETH_CONTRACT)).toBeTruthy();
});
itDoesIfSupported(ProviderMethod.EstimateGas, async () => {
@ -167,6 +159,25 @@ describe('SmartProvider', () => {
expect(result).toBe('0x0000000000000000000000000000000000000000000000000000000000000000');
});
it('Handles parallel requests', async () => {
const result1Promise = provider.getLogs({
address: WETH_CONTRACT,
topics: [WETH_TRANSFER_TOPIC0],
fromBlock: MIN_BLOCK_NUM,
toBlock: MIN_BLOCK_NUM + 100,
});
const result2Promise = provider.getBlockNumber();
const result3Promise = provider.getTransaction(TRANSFER_TX_HASH);
const [result1, result2, result3] = await Promise.all([
result1Promise,
result2Promise,
result3Promise,
]);
expect(result1).toBeTruthy();
expect(result2).toBeTruthy();
expect(result3).toBeTruthy();
});
//TODO
// itDoesIfSupported(ProviderMethod.SendTransaction, async () => {
// const result = await provider.sendTransaction('0x1234');

@ -2,9 +2,9 @@ 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 { isNullish } from '../../utils/typeof';
import { HyperlaneEtherscanProvider } from './HyperlaneEtherscanProvider';
import { HyperlaneJsonRpcProvider } from './HyperlaneJsonRpcProvider';
@ -14,12 +14,15 @@ import { ChainMetadataWithRpcConnectionInfo } from './types';
const PROVIDER_STAGGER_DELAY_MS = 1000; // 1 seconds
const PROVIDER_TIMEOUT_MARKER = '__PROVIDER_TIMEOUT__';
type HyperlaneProvider = HyperlaneEtherscanProvider | HyperlaneJsonRpcProvider;
export class HyperlaneSmartProvider extends providers.BaseProvider implements IProviderMethods {
public readonly chainMetadata: ChainMetadataWithRpcConnectionInfo;
// TODO also support blockscout here
public readonly explorerProviders: HyperlaneEtherscanProvider[];
public readonly rpcProviders: HyperlaneJsonRpcProvider[];
public readonly supportedMethods: ProviderMethod[];
public requestCount = 0;
constructor(chainMetadata: ChainMetadataWithRpcConnectionInfo) {
const network = chainMetadataToProviderNetwork(chainMetadata);
@ -75,47 +78,78 @@ export class HyperlaneSmartProvider extends providers.BaseProvider implements IP
);
if (!supportedProviders.length) throw new Error(`No providers available for method ${method}`);
let index = 0;
const maxIndex = supportedProviders.length - 1;
this.requestCount += 1;
const reqId = this.requestCount;
let pIndex = 0;
const maxPIndex = supportedProviders.length - 1;
const providerResultPromises: Promise<any>[] = [];
// TODO consider implementing quorum and/or retry logic here similar to FallbackProvider/RetryProvider
while (true) {
if (index <= maxIndex) {
if (pIndex <= maxPIndex) {
// Trigger the next provider in line
const provider = supportedProviders[index];
const provider = supportedProviders[pIndex];
const providerUrl = provider.getBaseUrl();
const resultPromise = provider.perform(method, params);
// Skip the explorer provider if it's currently in a cooldown period
if (
this.isExplorerProvider(provider) &&
provider.getQueryWaitTime() > 0 &&
pIndex < maxPIndex &&
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.race([resultPromise, timeoutPromise]);
const result = await Promise.any([resultPromise, timeoutPromise]);
if (isNullish(result)) {
logger.error(
`Nullish result from provider using ${providerUrl}. Triggering next available provider`,
);
index += 1;
} else if (result === PROVIDER_TIMEOUT_MARKER) {
if (result === PROVIDER_TIMEOUT_MARKER) {
logger.warn(
`Slow response from provider using ${providerUrl}. Triggering next available provider`,
`Slow response from provider using ${providerUrl}. Triggering next provider if available`,
);
index += 1;
pIndex += 1;
} else {
// Result looks good
return result;
}
} else {
// All providers already triggered, wait for one to complete
const timeoutPromise = sleep(PROVIDER_STAGGER_DELAY_MS * 12, PROVIDER_TIMEOUT_MARKER);
const result = await Promise.race([...providerResultPromises, timeoutPromise]);
if (isNullish(result) || result === PROVIDER_TIMEOUT_MARKER) {
throw new Error(`All providers failed or timed out for method ${method}`);
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 {
return result;
}
}
}
}
isExplorerProvider(p: HyperlaneProvider): p is HyperlaneEtherscanProvider {
return this.explorerProviders.includes(p as any);
}
}
function performWithLogging(
provider: HyperlaneProvider,
providerUrl: string,
method: string,
params: any,
reqId: number,
): Promise<any> {
try {
logger.debug(`Provider using ${providerUrl} performing method ${method} for reqId ${reqId}`);
return provider.perform(method, params, reqId);
} 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}`);
}
}
function chainMetadataToProviderNetwork(chainMetadata: ChainMetadata): providers.Network {
return {
name: chainMetadata.name,

@ -1,13 +1,44 @@
import { useMemo } from 'react';
import { MultiProvider, chainMetadata } from '@hyperlane-xyz/sdk';
import {
ChainName,
CoreChainName,
MultiProvider,
TestChains,
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;
// TODO fix when sdk is updated
const providers = this['providers'];
if (providers[name]) return providers[name];
if (TestChains.includes(name as CoreChainName)) {
providers[name] = new providers.JsonRpcProvider('http://localhost:8545', 31337);
} else if (publicRpcUrls?.length || blockExplorers?.length) {
providers[name] = new HyperlaneSmartProvider(metadata);
} else {
return null;
}
return providers[name];
}
}
export function useMultiProvider() {
const nameToConfig = useChainConfigsWithQueryParams();
const multiProvider = useMemo(
() => new MultiProvider({ ...chainMetadata, ...nameToConfig }),
() => new SmartMultiProvider({ ...chainMetadata, ...nameToConfig }),
[nameToConfig],
);
return multiProvider;

@ -1,3 +1,4 @@
import { logger } from './logger';
import { trimToLength } from './string';
export function errorToString(error: any, maxLength = 300) {
@ -8,3 +9,8 @@ export function errorToString(error: any, maxLength = 300) {
if (typeof details === 'string') return trimToLength(details, maxLength);
return trimToLength(JSON.stringify(details), maxLength);
}
export function logAndThrow(message: string, error?: any) {
logger.error(message, error);
throw new Error(message);
}

@ -9,7 +9,7 @@ import { logger } from './logger';
import { toDecimalNumber, tryToDecimalNumber } from './number';
import { fetchWithTimeout, sleep } from './timeout';
const BLOCK_EXPLORER_RATE_LIMIT = 5100; // once every 5.1 seconds
const BLOCK_EXPLORER_RATE_LIMIT = 6000; // once every 6 seconds
// Used for crude rate-limiting of explorer queries without API keys
const hostToLastQueried: Record<string, number> = {};

@ -1294,14 +1294,14 @@ __metadata:
languageName: node
linkType: hard
"@hyperlane-xyz/core@npm:1.3.2":
version: 1.3.2
resolution: "@hyperlane-xyz/core@npm:1.3.2"
"@hyperlane-xyz/core@npm:1.3.3":
version: 1.3.3
resolution: "@hyperlane-xyz/core@npm:1.3.3"
dependencies:
"@hyperlane-xyz/utils": 1.3.2
"@hyperlane-xyz/utils": 1.3.3
"@openzeppelin/contracts": ^4.8.0
"@openzeppelin/contracts-upgradeable": ^4.8.0
checksum: f195319c458c8d43d49e06790bb975c1940c06cab7e7c4d99ac9128c39b8c4c0c10b806c4bef1d5f2a97162fead998782a7e1292bc968751a824a89ad90c90ff
checksum: d21ede2e49e1152c518c7c89ff1c19811ae3eb78aaacf02dd7db028cbcc436b25bc39c77e2c93d7a08790dd32f5f9f921de4e0e0fccf0a1c8b8a7285cab18e01
languageName: node
linkType: hard
@ -1310,8 +1310,8 @@ __metadata:
resolution: "@hyperlane-xyz/explorer@workspace:."
dependencies:
"@headlessui/react": ^1.7.11
"@hyperlane-xyz/sdk": 1.3.2
"@hyperlane-xyz/widgets": 1.3.2
"@hyperlane-xyz/sdk": 1.3.3
"@hyperlane-xyz/widgets": 1.3.3
"@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6"
"@rainbow-me/rainbowkit": ^0.11.0
"@tanstack/react-query": ^4.24.10
@ -1349,12 +1349,12 @@ __metadata:
languageName: unknown
linkType: soft
"@hyperlane-xyz/sdk@npm:1.3.2":
version: 1.3.2
resolution: "@hyperlane-xyz/sdk@npm:1.3.2"
"@hyperlane-xyz/sdk@npm:1.3.3":
version: 1.3.3
resolution: "@hyperlane-xyz/sdk@npm:1.3.3"
dependencies:
"@hyperlane-xyz/core": 1.3.2
"@hyperlane-xyz/utils": 1.3.2
"@hyperlane-xyz/core": 1.3.3
"@hyperlane-xyz/utils": 1.3.3
"@types/coingecko-api": ^1.0.10
"@types/debug": ^4.1.7
"@wagmi/chains": ^0.2.6
@ -1363,27 +1363,27 @@ __metadata:
debug: ^4.3.4
ethers: ^5.7.2
zod: ^3.21.2
checksum: b2e54374eab564505454eaf51ea270758b1b7331c9e879bd0084c28d339e099ae68715b937001092689a526f4b2fa1eb448c2a836e9bfd9923a5fd4cf2564284
checksum: c54f9fc6b6ae93f00651d345591cf2945b8a3516b15b6f7a649b210abc1ed3c0725bbc80a0d2d5240cbe988bb21b10f7bb1f0c13348d3d3e2c4c1e2fe856c093
languageName: node
linkType: hard
"@hyperlane-xyz/utils@npm:1.3.2":
version: 1.3.2
resolution: "@hyperlane-xyz/utils@npm:1.3.2"
"@hyperlane-xyz/utils@npm:1.3.3":
version: 1.3.3
resolution: "@hyperlane-xyz/utils@npm:1.3.3"
dependencies:
ethers: ^5.7.2
checksum: e7cdeb4b14ccabbd46f46d0329d81792c5483e98463a5080c884bc8bff833aa8160c2ab107cda843a3f6c23979d9ca7b6d314c2cfc393c786cbf5fc2f8680934
checksum: 1141ff5f8c7559f8727c63782b37cf0f9444942e3f48059fad7dde533b160137a15bcfcc9f590ae3ee56faf33b1da4f7c99f8d32134792acd94c41fc235f19c1
languageName: node
linkType: hard
"@hyperlane-xyz/widgets@npm:1.3.2":
version: 1.3.2
resolution: "@hyperlane-xyz/widgets@npm:1.3.2"
"@hyperlane-xyz/widgets@npm:1.3.3":
version: 1.3.3
resolution: "@hyperlane-xyz/widgets@npm:1.3.3"
peerDependencies:
"@hyperlane-xyz/sdk": ^1.3.2
"@hyperlane-xyz/sdk": ^1.3.3
react: ^18
react-dom: ^18
checksum: 990e68273a495b99a1cac6cae13998692775b11c62a5508591ff6836661e3f6de94bf1b4db1532c3d560cc7759e7266146178473bf8dc3b616aa974e36171c6a
checksum: 30558e8b5dfa768259151c0a675c219fce5bdd64c30d7cb1c4ea41bad1bc9ff951b920c3eb26548de56c87097b9d4fea7fa20f5b893a542834b595c4459bc280
languageName: node
linkType: hard

Loading…
Cancel
Save