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 },
"contracts": {
"mailbox": "0x123..."
"mailbox": "0x123...",
"interchainGasPaymaster": "0x123..."
}
}
`;

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

@ -347,6 +347,7 @@ async function debugMessageDelivery(
const recipientContract = IMessageRecipient__factory.connect(recipient, destProvider);
try {
// 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
const deliveryGasEst = await recipientContract.estimateGas.handle(
originDomain,

@ -2,7 +2,7 @@ import { MultiProvider, chainMetadata, hyperlaneCoreAddresses } from '@hyperlane
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
// 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 { Mailbox__factory } from '@hyperlane-xyz/core';
@ -6,14 +5,8 @@ import { MultiProvider } from '@hyperlane-xyz/sdk';
import { utils } from '@hyperlane-xyz/utils';
import { getMultiProvider } from '../../../multiProvider';
import { useStore } from '../../../store';
import { LogWithTimestamp, Message, MessageStatus } from '../../../types';
import {
ensureLeading0x,
isValidAddress,
isValidTransactionHash,
normalizeAddress,
} from '../../../utils/addresses';
import { ExtendedLog, Message, MessageStatus } from '../../../types';
import { isValidAddress, isValidTransactionHash, normalizeAddress } from '../../../utils/addresses';
import {
queryExplorerForBlock,
queryExplorerForLogs,
@ -23,8 +16,6 @@ import {
import { logger } from '../../../utils/logger';
import { ChainConfig } from '../../chains/chainConfig';
import { isValidSearchQuery } from './useMessageQuery';
const PROVIDER_LOGS_BLOCK_WINDOW = 100_000;
const PROVIDER_BLOCK_DETAILS_WINDOW = 5_000;
@ -34,44 +25,10 @@ const dispatchIdTopic0 = mailbox.getEventTopic('DispatchId');
// const processTopic0 = mailbox.getEventTopic('Process');
// const processIdTopic0 = mailbox.getEventTopic('ProcessId');
// 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 found for:', sanitizedInput, e);
return [];
}
},
{ retry: false },
);
return {
isFetching: isLoading,
isError,
hasRun: !!data,
messageList: data || [],
};
export interface PiMessageQuery {
input: string;
fromBlock?: string | number;
toBlock?: string | number;
}
/* Pseudo-code for the fetch algo below:
@ -101,23 +58,6 @@ searchForMessages(input):
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(
chainConfig: ChainConfig,
query: PiMessageQuery,
@ -126,7 +66,7 @@ export async function fetchMessagesFromPiChain(
const useExplorer = !!chainConfig.blockExplorers?.[0]?.apiUrl;
const input = query.input;
let logs: LogWithTimestamp[];
let logs: ExtendedLog[];
if (isValidAddress(input)) {
logs = await fetchLogsForAddress(chainConfig, query, multiProvider, useExplorer);
} else if (isValidTransactionHash(input)) {
@ -140,7 +80,16 @@ export async function fetchMessagesFromPiChain(
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(
@ -148,7 +97,7 @@ async function fetchLogsForAddress(
query: PiMessageQuery,
multiProvider: MultiProvider,
useExplorer?: boolean,
): Promise<LogWithTimestamp[]> {
): Promise<ExtendedLog[]> {
const address = query.input;
logger.debug(`Fetching logs for address ${address} on chain ${chainId}`);
const mailboxAddr = contracts.mailbox;
@ -186,7 +135,7 @@ async function fetchLogsForTxHash(
query: PiMessageQuery,
multiProvider: MultiProvider,
useExplorer: boolean,
): Promise<LogWithTimestamp[]> {
): Promise<ExtendedLog[]> {
const txHash = query.input;
logger.debug(`Fetching logs for txHash ${txHash} on chain ${chainId}`);
if (useExplorer) {
@ -201,7 +150,7 @@ async function fetchLogsForTxHash(
);
return txReceipt.logs.map((l) => ({
...l,
timestamp: BigNumber.from(block.timestamp).toNumber() * 1000,
timestamp: parseBlockTimestamp(block),
from: txReceipt.from,
to: txReceipt.to,
}));
@ -214,11 +163,9 @@ async function fetchLogsForTxHash(
if (txReceipt) {
logger.debug(`Tx receipt found from provider for chain ${chainId}`);
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) => ({
...l,
timestamp,
timestamp: parseBlockTimestamp(block),
from: txReceipt.from,
to: txReceipt.to,
}));
@ -234,13 +181,13 @@ async function fetchLogsForMsgId(
query: PiMessageQuery,
multiProvider: MultiProvider,
useExplorer: boolean,
): Promise<LogWithTimestamp[]> {
): 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: LogWithTimestamp[];
let logs: ExtendedLog[];
if (useExplorer) {
logs = await fetchLogsFromExplorer(
[
@ -284,11 +231,11 @@ async function fetchLogsFromExplorer(
chainId: number,
query: PiMessageQuery,
multiProvider: MultiProvider,
): Promise<LogWithTimestamp[]> {
): 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: LogWithTimestamp[] = [];
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);
@ -303,7 +250,7 @@ async function fetchLogsFromProvider(
chainId: number,
query: PiMessageQuery,
multiProvider: MultiProvider,
): Promise<LogWithTimestamp[]> {
): Promise<ExtendedLog[]> {
const provider = multiProvider.getProvider(chainId);
const latestBlock = await provider.getBlockNumber();
const fromBlock = query.fromBlock || latestBlock - PROVIDER_LOGS_BLOCK_WINDOW;
@ -322,15 +269,13 @@ async function fetchLogsFromProvider(
)
).flat();
const timestamps: Record<number, number> = {};
const logsWithTimestamp = await Promise.all<LogWithTimestamp>(
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);
// TODO make timestamps optional instead of using 0 fallback here
const timestamp = block ? BigNumber.from(block.timestamp).toNumber() * 1000 : 0;
timestamps[blockNum] = timestamp;
timestamps[blockNum] = parseBlockTimestamp(block);
}
return {
...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;
try {
logDesc = mailbox.parseLog(log);
@ -390,7 +340,7 @@ function logToMessage(log: LogWithTimestamp, chainConfig: ChainConfig): Message
destinationDomainId: message.destination,
body: message.body,
origin: {
timestamp: log.timestamp,
timestamp: log.timestamp || 0,
hash: log.transactionHash,
from: log.from ? normalizeAddress(log.from) : constants.AddressZero,
to: log.to ? normalizeAddress(log.to) : constants.AddressZero,
@ -414,3 +364,23 @@ function logToMessage(log: LogWithTimestamp, chainConfig: ChainConfig): Message
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);
}

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

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

Loading…
Cancel
Save