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