Start impl of IGP gas fetching for PI messages

Refactor some of PI message search code
pi-gas-details
J M Rossy 2 years ago
parent 1fb99eb0bd
commit 34b7eda10f
  1. 3
      src/features/chains/ConfigureChains.tsx
  2. 4
      src/features/chains/chainConfig.ts
  3. 1
      src/features/debugger/debugMessage.ts
  4. 2
      src/features/messages/pi-queries/fetchPiChainMessages.test.ts
  5. 142
      src/features/messages/pi-queries/fetchPiChainMessages.ts
  6. 64
      src/features/messages/pi-queries/usePiChainMessageQuery.ts
  7. 2
      src/multiProvider.ts
  8. 4
      src/types.ts
  9. 4
      src/utils/explorers.ts

@ -178,7 +178,8 @@ const customChainTextareaPlaceholder = `{
} ], } ],
"blocks": { "confirmations": 1, "estimateBlockTime": 13 }, "blocks": { "confirmations": 1, "estimateBlockTime": 13 },
"contracts": { "contracts": {
"mailbox": "0x123..." "mailbox": "0x123...",
"interchainGasPaymaster": "0x123..."
} }
} }
`; `;

@ -8,7 +8,7 @@ import { logger } from '../../utils/logger';
export const chainContractsSchema = z.object({ export const chainContractsSchema = z.object({
mailbox: z.string(), mailbox: z.string(),
multisigIsm: z.string().optional(), multisigIsm: z.string().optional(),
// interchainGasPaymaster: z.string().optional(), interchainGasPaymaster: z.string().optional(),
// interchainAccountRouter: z.string().optional(), // interchainAccountRouter: z.string().optional(),
}); });
@ -52,7 +52,7 @@ export function tryParseChainConfig(input: string): ParseResult {
const chainConfig = result.data as ChainConfig; const chainConfig = result.data as ChainConfig;
// Reject blocksout explorers for now // Reject blockscout explorers for now
if (chainConfig.blockExplorers?.[0]?.url.includes('blockscout')) { if (chainConfig.blockExplorers?.[0]?.url.includes('blockscout')) {
return { return {
success: false, success: false,

@ -347,6 +347,7 @@ async function debugMessageDelivery(
const recipientContract = IMessageRecipient__factory.connect(recipient, destProvider); const recipientContract = IMessageRecipient__factory.connect(recipient, destProvider);
try { try {
// TODO add special case for Arbitrum: // TODO add special case for Arbitrum:
// TODO account for mailbox handling gas overhead
// https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/1949/files#diff-79ec1cf679507919c08a9a66e0407c16fff22aee98d79cf39a0c1baf086403ebR364 // https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/1949/files#diff-79ec1cf679507919c08a9a66e0407c16fff22aee98d79cf39a0c1baf086403ebR364
const deliveryGasEst = await recipientContract.estimateGas.handle( const deliveryGasEst = await recipientContract.estimateGas.handle(
originDomain, originDomain,

@ -2,7 +2,7 @@ import { MultiProvider, chainMetadata, hyperlaneCoreAddresses } from '@hyperlane
import { ChainConfig } from '../../chains/chainConfig'; import { ChainConfig } from '../../chains/chainConfig';
import { fetchMessagesFromPiChain } from './usePiChainMessageQuery'; import { fetchMessagesFromPiChain } from './fetchPiChainMessages';
// NOTE: THESE TESTS WILL NO LONGER WORK ONCE THE MESSAGE USED BELOW // NOTE: THESE TESTS WILL NO LONGER WORK ONCE THE MESSAGE USED BELOW
// IS OUT OF PROVIDER_LOGS_BLOCK_WINDOW USED TO QUERY // IS OUT OF PROVIDER_LOGS_BLOCK_WINDOW USED TO QUERY

@ -1,4 +1,3 @@
import { useQuery } from '@tanstack/react-query';
import { BigNumber, constants, ethers, providers } from 'ethers'; import { BigNumber, constants, ethers, providers } from 'ethers';
import { Mailbox__factory } from '@hyperlane-xyz/core'; import { Mailbox__factory } from '@hyperlane-xyz/core';
@ -6,14 +5,8 @@ import { MultiProvider } from '@hyperlane-xyz/sdk';
import { utils } from '@hyperlane-xyz/utils'; import { utils } from '@hyperlane-xyz/utils';
import { getMultiProvider } from '../../../multiProvider'; import { getMultiProvider } from '../../../multiProvider';
import { useStore } from '../../../store'; import { ExtendedLog, Message, MessageStatus } from '../../../types';
import { LogWithTimestamp, Message, MessageStatus } from '../../../types'; import { isValidAddress, isValidTransactionHash, normalizeAddress } from '../../../utils/addresses';
import {
ensureLeading0x,
isValidAddress,
isValidTransactionHash,
normalizeAddress,
} from '../../../utils/addresses';
import { import {
queryExplorerForBlock, queryExplorerForBlock,
queryExplorerForLogs, queryExplorerForLogs,
@ -23,8 +16,6 @@ import {
import { logger } from '../../../utils/logger'; import { logger } from '../../../utils/logger';
import { ChainConfig } from '../../chains/chainConfig'; import { ChainConfig } from '../../chains/chainConfig';
import { isValidSearchQuery } from './useMessageQuery';
const PROVIDER_LOGS_BLOCK_WINDOW = 100_000; const PROVIDER_LOGS_BLOCK_WINDOW = 100_000;
const PROVIDER_BLOCK_DETAILS_WINDOW = 5_000; const PROVIDER_BLOCK_DETAILS_WINDOW = 5_000;
@ -34,44 +25,10 @@ const dispatchIdTopic0 = mailbox.getEventTopic('DispatchId');
// const processTopic0 = mailbox.getEventTopic('Process'); // const processTopic0 = mailbox.getEventTopic('Process');
// const processIdTopic0 = mailbox.getEventTopic('ProcessId'); // const processIdTopic0 = mailbox.getEventTopic('ProcessId');
// Query 'Permissionless Interoperability (PI)' chains using custom export interface PiMessageQuery {
// chain configs in store state input: string;
export function usePiChainMessageQuery( fromBlock?: string | number;
sanitizedInput: string, toBlock?: string | number;
startTimeFilter: number | null,
endTimeFilter: number | null,
pause: boolean,
) {
const chainConfigs = useStore((s) => s.chainConfigs);
const { isLoading, isError, data } = useQuery(
['usePiChainMessageQuery', chainConfigs, sanitizedInput, startTimeFilter, endTimeFilter, pause],
async () => {
const hasInput = !!sanitizedInput;
const isValidInput = isValidSearchQuery(sanitizedInput, true);
if (pause || !hasInput || !isValidInput || !Object.keys(chainConfigs).length) return [];
logger.debug('Starting PI Chain message query for:', sanitizedInput);
// TODO convert timestamps to from/to blocks here
const query = { input: ensureLeading0x(sanitizedInput) };
const multiProvider = getMultiProvider();
try {
const messages = await Promise.any(
Object.values(chainConfigs).map((c) => fetchMessagesOrThrow(c, query, multiProvider)),
);
return messages;
} catch (e) {
logger.debug('Error fetching PI messages found for:', sanitizedInput, e);
return [];
}
},
{ retry: false },
);
return {
isFetching: isLoading,
isError,
hasRun: !!data,
messageList: data || [],
};
} }
/* Pseudo-code for the fetch algo below: /* Pseudo-code for the fetch algo below:
@ -101,23 +58,6 @@ searchForMessages(input):
GOTO hash search above GOTO hash search above
*/ */
export interface PiMessageQuery {
input: string;
fromBlock?: string | number;
toBlock?: string | number;
}
async function fetchMessagesOrThrow(
chainConfig: ChainConfig,
query: PiMessageQuery,
multiProvider: MultiProvider,
): Promise<Message[]> {
const messages = await fetchMessagesFromPiChain(chainConfig, query, multiProvider);
// Throw so Promise.any caller doesn't trigger
if (!messages.length) throw new Error(`No messages found for chain ${chainConfig.chainId}`);
return messages;
}
export async function fetchMessagesFromPiChain( export async function fetchMessagesFromPiChain(
chainConfig: ChainConfig, chainConfig: ChainConfig,
query: PiMessageQuery, query: PiMessageQuery,
@ -126,7 +66,7 @@ export async function fetchMessagesFromPiChain(
const useExplorer = !!chainConfig.blockExplorers?.[0]?.apiUrl; const useExplorer = !!chainConfig.blockExplorers?.[0]?.apiUrl;
const input = query.input; const input = query.input;
let logs: LogWithTimestamp[]; let logs: ExtendedLog[];
if (isValidAddress(input)) { if (isValidAddress(input)) {
logs = await fetchLogsForAddress(chainConfig, query, multiProvider, useExplorer); logs = await fetchLogsForAddress(chainConfig, query, multiProvider, useExplorer);
} else if (isValidTransactionHash(input)) { } else if (isValidTransactionHash(input)) {
@ -140,7 +80,16 @@ export async function fetchMessagesFromPiChain(
return []; return [];
} }
return logs.map((l) => logToMessage(l, chainConfig)).filter((m): m is Message => !!m); const messages = logs.map((l) => logToMessage(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, useExplorer),
);
}
return messagesWithGasPayments;
} }
async function fetchLogsForAddress( async function fetchLogsForAddress(
@ -148,7 +97,7 @@ async function fetchLogsForAddress(
query: PiMessageQuery, query: PiMessageQuery,
multiProvider: MultiProvider, multiProvider: MultiProvider,
useExplorer?: boolean, useExplorer?: boolean,
): Promise<LogWithTimestamp[]> { ): Promise<ExtendedLog[]> {
const address = query.input; const address = query.input;
logger.debug(`Fetching logs for address ${address} on chain ${chainId}`); logger.debug(`Fetching logs for address ${address} on chain ${chainId}`);
const mailboxAddr = contracts.mailbox; const mailboxAddr = contracts.mailbox;
@ -186,7 +135,7 @@ async function fetchLogsForTxHash(
query: PiMessageQuery, query: PiMessageQuery,
multiProvider: MultiProvider, multiProvider: MultiProvider,
useExplorer: boolean, useExplorer: boolean,
): Promise<LogWithTimestamp[]> { ): Promise<ExtendedLog[]> {
const txHash = query.input; const txHash = query.input;
logger.debug(`Fetching logs for txHash ${txHash} on chain ${chainId}`); logger.debug(`Fetching logs for txHash ${txHash} on chain ${chainId}`);
if (useExplorer) { if (useExplorer) {
@ -201,7 +150,7 @@ async function fetchLogsForTxHash(
); );
return txReceipt.logs.map((l) => ({ return txReceipt.logs.map((l) => ({
...l, ...l,
timestamp: BigNumber.from(block.timestamp).toNumber() * 1000, timestamp: parseBlockTimestamp(block),
from: txReceipt.from, from: txReceipt.from,
to: txReceipt.to, to: txReceipt.to,
})); }));
@ -214,11 +163,9 @@ async function fetchLogsForTxHash(
if (txReceipt) { if (txReceipt) {
logger.debug(`Tx receipt found from provider for chain ${chainId}`); logger.debug(`Tx receipt found from provider for chain ${chainId}`);
const block = await tryFetchBlockFromProvider(provider, txReceipt.blockNumber); const block = await tryFetchBlockFromProvider(provider, txReceipt.blockNumber);
// TODO make timestamp optional instead of using 0 fallback here
const timestamp = block ? BigNumber.from(block.timestamp).toNumber() * 1000 : 0;
return txReceipt.logs.map((l) => ({ return txReceipt.logs.map((l) => ({
...l, ...l,
timestamp, timestamp: parseBlockTimestamp(block),
from: txReceipt.from, from: txReceipt.from,
to: txReceipt.to, to: txReceipt.to,
})); }));
@ -234,13 +181,13 @@ async function fetchLogsForMsgId(
query: PiMessageQuery, query: PiMessageQuery,
multiProvider: MultiProvider, multiProvider: MultiProvider,
useExplorer: boolean, useExplorer: boolean,
): Promise<LogWithTimestamp[]> { ): Promise<ExtendedLog[]> {
const { contracts, chainId } = chainConfig; const { contracts, chainId } = chainConfig;
const msgId = query.input; const msgId = query.input;
logger.debug(`Fetching logs for msgId ${msgId} on chain ${chainId}`); logger.debug(`Fetching logs for msgId ${msgId} on chain ${chainId}`);
const mailboxAddr = contracts.mailbox; const mailboxAddr = contracts.mailbox;
const topic1 = msgId; const topic1 = msgId;
let logs: LogWithTimestamp[]; let logs: ExtendedLog[];
if (useExplorer) { if (useExplorer) {
logs = await fetchLogsFromExplorer( logs = await fetchLogsFromExplorer(
[ [
@ -284,11 +231,11 @@ async function fetchLogsFromExplorer(
chainId: number, chainId: number,
query: PiMessageQuery, query: PiMessageQuery,
multiProvider: MultiProvider, multiProvider: MultiProvider,
): Promise<LogWithTimestamp[]> { ): Promise<ExtendedLog[]> {
const fromBlock = query.fromBlock || '1'; const fromBlock = query.fromBlock || '1';
const toBlock = query.toBlock || 'latest'; const toBlock = query.toBlock || 'latest';
const base = `module=logs&action=getLogs&fromBlock=${fromBlock}&toBlock=${toBlock}&address=${contractAddr}`; const base = `module=logs&action=getLogs&fromBlock=${fromBlock}&toBlock=${toBlock}&address=${contractAddr}`;
let logs: LogWithTimestamp[] = []; let logs: ExtendedLog[] = [];
for (const path of paths) { for (const path of paths) {
// Originally use parallel requests here with Promise.all but immediately hit rate limit errors // Originally use parallel requests here with Promise.all but immediately hit rate limit errors
const result = await queryExplorerForLogs(multiProvider, chainId, `${base}${path}`, false); const result = await queryExplorerForLogs(multiProvider, chainId, `${base}${path}`, false);
@ -303,7 +250,7 @@ async function fetchLogsFromProvider(
chainId: number, chainId: number,
query: PiMessageQuery, query: PiMessageQuery,
multiProvider: MultiProvider, multiProvider: MultiProvider,
): Promise<LogWithTimestamp[]> { ): Promise<ExtendedLog[]> {
const provider = multiProvider.getProvider(chainId); const provider = multiProvider.getProvider(chainId);
const latestBlock = await provider.getBlockNumber(); const latestBlock = await provider.getBlockNumber();
const fromBlock = query.fromBlock || latestBlock - PROVIDER_LOGS_BLOCK_WINDOW; const fromBlock = query.fromBlock || latestBlock - PROVIDER_LOGS_BLOCK_WINDOW;
@ -322,15 +269,13 @@ async function fetchLogsFromProvider(
) )
).flat(); ).flat();
const timestamps: Record<number, number> = {}; const timestamps: Record<number, number | undefined> = {};
const logsWithTimestamp = await Promise.all<LogWithTimestamp>( const logsWithTimestamp = await Promise.all<ExtendedLog>(
logs.map(async (l) => { logs.map(async (l) => {
const blockNum = l.blockNumber; const blockNum = l.blockNumber;
if (!timestamps[blockNum]) { if (!timestamps[blockNum]) {
const block = await tryFetchBlockFromProvider(provider, blockNum, latestBlock); const block = await tryFetchBlockFromProvider(provider, blockNum, latestBlock);
// TODO make timestamps optional instead of using 0 fallback here timestamps[blockNum] = parseBlockTimestamp(block);
const timestamp = block ? BigNumber.from(block.timestamp).toNumber() * 1000 : 0;
timestamps[blockNum] = timestamp;
} }
return { return {
...l, ...l,
@ -357,7 +302,12 @@ async function tryFetchBlockFromProvider(
} }
} }
function logToMessage(log: LogWithTimestamp, chainConfig: ChainConfig): Message | null { function parseBlockTimestamp(block: providers.Block | null): number | undefined {
if (!block) return undefined;
return BigNumber.from(block.timestamp).toNumber() * 1000;
}
function logToMessage(log: ExtendedLog, chainConfig: ChainConfig): Message | null {
let logDesc: ethers.utils.LogDescription; let logDesc: ethers.utils.LogDescription;
try { try {
logDesc = mailbox.parseLog(log); logDesc = mailbox.parseLog(log);
@ -390,7 +340,7 @@ function logToMessage(log: LogWithTimestamp, chainConfig: ChainConfig): Message
destinationDomainId: message.destination, destinationDomainId: message.destination,
body: message.body, body: message.body,
origin: { origin: {
timestamp: log.timestamp, timestamp: log.timestamp || 0,
hash: log.transactionHash, hash: log.transactionHash,
from: log.from ? normalizeAddress(log.from) : constants.AddressZero, from: log.from ? normalizeAddress(log.from) : constants.AddressZero,
to: log.to ? normalizeAddress(log.to) : constants.AddressZero, to: log.to ? normalizeAddress(log.to) : constants.AddressZero,
@ -414,3 +364,23 @@ function logToMessage(log: LogWithTimestamp, chainConfig: ChainConfig): Message
return null; return null;
} }
} }
// Fetch and sum all IGP gas payments for a given message
async function tryFetchIgpGasPayments(
message: Message,
chainConfig: ChainConfig,
multiProvider: MultiProvider,
useExplorer?: boolean,
): Promise<Message> {
const { chainId, contracts } = chainConfig;
const igpAddr = contracts.interchainGasPaymaster;
if (!igpAddr || !isValidAddress(igpAddr)) {
logger.debug('No IGP address found for chain:', chainId);
return message;
}
// Mimic logic in debugger's tryCheckIgpGasFunded
// Either duplicate or refactor into shared util built on SmartProvider
return message;
}

@ -0,0 +1,64 @@
import { useQuery } from '@tanstack/react-query';
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { getMultiProvider } from '../../../multiProvider';
import { useStore } from '../../../store';
import { Message } from '../../../types';
import { ensureLeading0x } from '../../../utils/addresses';
import { logger } from '../../../utils/logger';
import { ChainConfig } from '../../chains/chainConfig';
import { isValidSearchQuery } from '../queries/useMessageQuery';
import { PiMessageQuery, fetchMessagesFromPiChain } from './fetchPiChainMessages';
// Query 'Permissionless Interoperability (PI)' chains using custom
// chain configs in store state
export function usePiChainMessageQuery(
sanitizedInput: string,
startTimeFilter: number | null,
endTimeFilter: number | null,
pause: boolean,
) {
const chainConfigs = useStore((s) => s.chainConfigs);
const { isLoading, isError, data } = useQuery(
['usePiChainMessageQuery', chainConfigs, sanitizedInput, startTimeFilter, endTimeFilter, pause],
async () => {
const hasInput = !!sanitizedInput;
const isValidInput = isValidSearchQuery(sanitizedInput, true);
if (pause || !hasInput || !isValidInput || !Object.keys(chainConfigs).length) return [];
logger.debug('Starting PI Chain message query for:', sanitizedInput);
// TODO convert timestamps to from/to blocks here
const query = { input: ensureLeading0x(sanitizedInput) };
const multiProvider = getMultiProvider();
try {
const messages = await Promise.any(
Object.values(chainConfigs).map((c) => fetchMessagesOrThrow(c, query, multiProvider)),
);
return messages;
} catch (e) {
logger.debug('Error fetching PI messages for:', sanitizedInput, e);
return [];
}
},
{ retry: false },
);
return {
isFetching: isLoading,
isError,
hasRun: !!data,
messageList: data || [],
};
}
async function fetchMessagesOrThrow(
chainConfig: ChainConfig,
query: PiMessageQuery,
multiProvider: MultiProvider,
): Promise<Message[]> {
const messages = await fetchMessagesFromPiChain(chainConfig, query, multiProvider);
// Throw so Promise.any caller doesn't trigger
if (!messages.length) throw new Error(`No messages found for chain ${chainConfig.chainId}`);
return messages;
}

@ -18,6 +18,6 @@ export function setMultiProviderChains(customChainConfigs: Record<number, ChainC
}); });
} }
export function getProvider(chainId) { export function getProvider(chainId: number) {
return getMultiProvider().getProvider(chainId); return getMultiProvider().getProvider(chainId);
} }

@ -55,8 +55,8 @@ export interface Message extends MessageStub {
numPayments?: number; numPayments?: number;
} }
export interface LogWithTimestamp extends providers.Log { export interface ExtendedLog extends providers.Log {
timestamp: number; timestamp?: number;
from?: Address; from?: Address;
to?: Address; to?: Address;
} }

@ -3,7 +3,7 @@ import { BigNumber, providers } from 'ethers';
import { MultiProvider } from '@hyperlane-xyz/sdk'; import { MultiProvider } from '@hyperlane-xyz/sdk';
import { config } from '../consts/config'; import { config } from '../consts/config';
import type { LogWithTimestamp } from '../types'; import type { ExtendedLog } from '../types';
import { logger } from './logger'; import { logger } from './logger';
import { toDecimalNumber, tryToDecimalNumber } from './number'; import { toDecimalNumber, tryToDecimalNumber } from './number';
@ -113,7 +113,7 @@ function validateExplorerLog(log: ExplorerLogEntry) {
if (!log.timeStamp) throw new Error('Log has no timestamp'); if (!log.timeStamp) throw new Error('Log has no timestamp');
} }
export function toProviderLog(log: ExplorerLogEntry): LogWithTimestamp { export function toProviderLog(log: ExplorerLogEntry): ExtendedLog {
return { return {
...log, ...log,
blockHash: '', blockHash: '',

Loading…
Cancel
Save