diff --git a/src/components/search/SearchFilterBar.tsx b/src/components/search/SearchFilterBar.tsx index 8e59d44..59edd00 100644 --- a/src/components/search/SearchFilterBar.tsx +++ b/src/components/search/SearchFilterBar.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import { useState } from 'react'; -import { ChainMetadata } from '@hyperlane-xyz/sdk'; +import { ChainMetadata, getDomainId } from '@hyperlane-xyz/sdk'; import { trimToLength } from '@hyperlane-xyz/utils'; import { ChevronIcon, IconButton, Popover, XIcon, useModal } from '@hyperlane-xyz/widgets'; @@ -69,7 +69,7 @@ function ChainSelector({ : undefined; const onClickChain = (c: ChainMetadata) => { - onChangeValue(c.chainId.toString()); + onChangeValue(getDomainId(c).toString()); close(); }; diff --git a/src/components/search/SearchStates.tsx b/src/components/search/SearchStates.tsx index a4c1ecc..f4e5d6f 100644 --- a/src/components/search/SearchStates.tsx +++ b/src/components/search/SearchStates.tsx @@ -76,7 +76,7 @@ export function SearchInvalidError({ show={show} imgSrc={SearchOffIcon} text={`Sorry, that search input is not valid. Please try ${ - allowAddress ? 'an account addresses or ' : '' + allowAddress ? 'an account address or ' : '' }a transaction hash like 0xABC123...`} imgWidth={70} /> diff --git a/src/features/chains/ChainSearchModal.tsx b/src/features/chains/ChainSearchModal.tsx index 65634fd..b2e84a8 100644 --- a/src/features/chains/ChainSearchModal.tsx +++ b/src/features/chains/ChainSearchModal.tsx @@ -3,7 +3,7 @@ import { ChainSearchMenu, Modal } from '@hyperlane-xyz/widgets'; import { useMultiProvider, useStore } from '../../store'; -import { useScrapedEvmChains } from './queries/useScrapedChains'; +import { useScrapedChains } from './queries/useScrapedChains'; export function ChainSearchModal({ isOpen, @@ -17,7 +17,7 @@ export function ChainSearchModal({ showAddChainMenu?: boolean; }) { const multiProvider = useMultiProvider(); - const { chains } = useScrapedEvmChains(multiProvider); + const { chains } = useScrapedChains(multiProvider); const { chainMetadataOverrides, setChainMetadataOverrides } = useStore((s) => ({ chainMetadataOverrides: s.chainMetadataOverrides, setChainMetadataOverrides: s.setChainMetadataOverrides, diff --git a/src/features/chains/queries/useScrapedChains.ts b/src/features/chains/queries/useScrapedChains.ts index 8cbce80..30347cb 100644 --- a/src/features/chains/queries/useScrapedChains.ts +++ b/src/features/chains/queries/useScrapedChains.ts @@ -6,51 +6,47 @@ import { objFilter } from '@hyperlane-xyz/utils'; import { unscrapedChainsInDb } from '../../../consts/config'; import { useStore } from '../../../store'; -import { isEvmChain, isPiChain } from '../utils'; +import { isPiChain } from '../utils'; import { DOMAINS_QUERY, DomainsEntry } from './fragments'; -export function useScrapedChains() { - const { scrapedChains, setScrapedChains } = useStore((s) => ({ - scrapedChains: s.scrapedChains, - setScrapedChains: s.setScrapedChains, +export function useScrapedDomains() { + const { scrapedDomains, setScrapedDomains } = useStore((s) => ({ + scrapedDomains: s.scrapedDomains, + setScrapedDomains: s.setScrapedDomains, })); const [result] = useQuery<{ domain: Array }>({ query: DOMAINS_QUERY, - pause: !!scrapedChains?.length, + pause: !!scrapedDomains?.length, }); const { data, fetching: isFetching, error } = result; useEffect(() => { if (!data) return; - setScrapedChains(data.domain); - }, [data, error, setScrapedChains]); + setScrapedDomains(data.domain); + }, [data, error, setScrapedDomains]); return { - scrapedChains, + scrapedDomains, isFetching, isError: !!error, }; } -export function useScrapedEvmChains(multiProvider: MultiProvider) { - const { scrapedChains, isFetching, isError } = useScrapedChains(); +export function useScrapedChains(multiProvider: MultiProvider) { + const { scrapedDomains, isFetching, isError } = useScrapedDomains(); const chainMetadata = useStore((s) => s.chainMetadata); const { chains } = useMemo(() => { - // Filtering to EVM is necessary to prevent errors until cosmos support is added - // https://github.com/hyperlane-xyz/hyperlane-explorer/issues/61 - const scrapedEvmChains = objFilter( + const scrapedChains = objFilter( chainMetadata, (_, chainMetadata): chainMetadata is ChainMetadata => - isEvmChain(multiProvider, chainMetadata.chainId) && - !isPiChain(multiProvider, scrapedChains, chainMetadata.chainId) && + !isPiChain(multiProvider, scrapedDomains, chainMetadata.chainId) && !isUnscrapedDbChain(multiProvider, chainMetadata.chainId), ); - // Return only evmChains because of graphql only accept query non-evm chains (with bigint type not string) - return { chains: scrapedEvmChains }; - }, [multiProvider, chainMetadata, scrapedChains]); + return { chains: scrapedChains }; + }, [multiProvider, chainMetadata, scrapedDomains]); return { chains, isFetching, isError }; } diff --git a/src/features/messages/cards/TransactionCard.tsx b/src/features/messages/cards/TransactionCard.tsx index 404553c..080431d 100644 --- a/src/features/messages/cards/TransactionCard.tsx +++ b/src/features/messages/cards/TransactionCard.tsx @@ -2,7 +2,7 @@ import BigNumber from 'bignumber.js'; import { PropsWithChildren, ReactNode, useState } from 'react'; import { MultiProvider } from '@hyperlane-xyz/sdk'; -import { isAddress, isZeroish } from '@hyperlane-xyz/utils'; +import { ProtocolType, isAddress, isZeroish, strip0x } from '@hyperlane-xyz/utils'; import { Modal, useModal } from '@hyperlane-xyz/widgets'; import { Spinner } from '../../../components/animations/Spinner'; @@ -127,38 +127,30 @@ export function DestinationTransactionCard({ ); - } else if (status === MessageStatus.Pending) { - if (isDestinationEvmChain) { - content = ( - -
-
Delivery to destination chain still in progress.
- {isPiMsg && ( -
- Please ensure a relayer is running for this chain. -
- )} - - -
-
- ); - } else { - content = ( - -
Sorry, delivery information is currently available for only EVM-type chains.
-
Support for other protocols is coming soon.
-
- ); - } + } else if (status === MessageStatus.Pending && isDestinationEvmChain) { + content = ( + +
+
Delivery to destination chain still in progress.
+ {isPiMsg && ( +
+ Please ensure a relayer is running for this chain. +
+ )} + + +
+
+ ); } else { content = ( -
{`Delivery to status is currently unknown. ${ - isPiMsg +
Delivery to status is currently unknown.
+
+ {isPiMsg ? 'Please ensure your chain config is correct and check back later.' - : 'Please check again later' - }`}
+ : 'Please check again later'} +
); } @@ -210,12 +202,14 @@ function TransactionDetails({ blur: boolean; }) { const multiProvider = useMultiProvider(); + const protocol = multiProvider.tryGetProtocol(domainId) || ProtocolType.Ethereum; const { hash, from, timestamp, blockNumber, mailbox } = transaction; + const formattedHash = protocol === ProtocolType.Cosmos ? strip0x(hash) : hash; const txExplorerLink = hash && !new BigNumber(hash).isZero() - ? multiProvider.tryGetExplorerTxUrl(chainId, { hash }) + ? multiProvider.tryGetExplorerTxUrl(chainId, { hash: formattedHash }) : null; return ( diff --git a/src/features/messages/pi-queries/usePiChainMessageQuery.ts b/src/features/messages/pi-queries/usePiChainMessageQuery.ts index 36ad163..99f0d16 100644 --- a/src/features/messages/pi-queries/usePiChainMessageQuery.ts +++ b/src/features/messages/pi-queries/usePiChainMessageQuery.ts @@ -7,7 +7,7 @@ import { ensure0x, timeout } from '@hyperlane-xyz/utils'; import { useReadyMultiProvider, useRegistry } from '../../../store'; import { Message } from '../../../types'; import { logger } from '../../../utils/logger'; -import { useScrapedChains } from '../../chains/queries/useScrapedChains'; +import { useScrapedDomains } from '../../chains/queries/useScrapedChains'; import { isEvmChain, isPiChain } from '../../chains/utils'; import { isValidSearchQuery } from '../queries/useMessageQuery'; @@ -30,7 +30,7 @@ export function usePiChainMessageSearchQuery({ piQueryType?: PiQueryType; pause: boolean; }) { - const { scrapedChains } = useScrapedChains(); + const { scrapedDomains: scrapedChains } = useScrapedDomains(); const multiProvider = useReadyMultiProvider(); const registry = useRegistry(); diff --git a/src/features/messages/queries/build.ts b/src/features/messages/queries/build.ts index 0a32878..46208e1 100644 --- a/src/features/messages/queries/build.ts +++ b/src/features/messages/queries/build.ts @@ -2,7 +2,7 @@ import { isAddress } from '@hyperlane-xyz/utils'; import { adjustToUtcTime } from '../../../utils/time'; -import { stringToPostgresBytea } from './encoding'; +import { searchValueToPostgresBytea } from './encoding'; import { messageDetailsFragment, messageStubFragment } from './fragments'; /** @@ -47,7 +47,7 @@ export function buildMessageQuery( } else { throw new Error(`Invalid id type: ${idType}`); } - const variables = { identifier: stringToPostgresBytea(idValue) }; + const variables = { identifier: searchValueToPostgresBytea(idValue) }; const query = ` query ($identifier: bytea!) @cached(ttl: 5) { @@ -78,7 +78,7 @@ export function buildMessageSearchQuery( const startTime = startTimeFilter ? adjustToUtcTime(startTimeFilter) : undefined; const endTime = endTimeFilter ? adjustToUtcTime(endTimeFilter) : undefined; const variables = { - search: hasInput ? stringToPostgresBytea(searchInput) : undefined, + search: hasInput ? searchValueToPostgresBytea(searchInput) : undefined, originChains, destinationChains, startTime, @@ -93,8 +93,8 @@ export function buildMessageSearchQuery( `q${i}: message_view( where: { _and: [ - ${originFilter ? '{origin_chain_id: {_in: $originChains}},' : ''} - ${destFilter ? '{destination_chain_id: {_in: $destinationChains}},' : ''} + ${originFilter ? '{origin_domain_id: {_in: $originChains}},' : ''} + ${destFilter ? '{destination_domain_id: {_in: $destinationChains}},' : ''} ${startTimeFilter ? '{send_occurred_at: {_gte: $startTime}},' : ''} ${endTimeFilter ? '{send_occurred_at: {_lte: $endTime}},' : ''} ${whereClause} @@ -107,7 +107,7 @@ export function buildMessageSearchQuery( }`, ); - const query = `query ($search: bytea, $originChains: [bigint!], $destinationChains: [bigint!], $startTime: timestamp, $endTime: timestamp) @cached(ttl: 5) { + const query = `query ($search: bytea, $originChains: [Int!], $destinationChains: [Int!], $startTime: timestamp, $endTime: timestamp) @cached(ttl: 5) { ${queries.join('\n')} }`; return { query, variables }; diff --git a/src/features/messages/queries/encoding.ts b/src/features/messages/queries/encoding.ts index 4223f87..55ce7b8 100644 --- a/src/features/messages/queries/encoding.ts +++ b/src/features/messages/queries/encoding.ts @@ -1,12 +1,39 @@ -import { ensure0x, strip0x } from '@hyperlane-xyz/utils'; +import { ChainMetadata } from '@hyperlane-xyz/sdk'; +import { + addressToByteHexString, + bytesToProtocolAddress, + ensure0x, + isAddress, + strip0x, +} from '@hyperlane-xyz/utils'; -export function stringToPostgresBytea(hexString: string) { +export function stringToPostgresBytea(hexString: string): string { const trimmed = strip0x(hexString).toLowerCase(); const prefix = `\\x`; return `${prefix}${trimmed}`; } -export function postgresByteaToString(byteString: string) { +export function postgresByteaToString(byteString: string): string { if (!byteString || byteString.length < 4) throw new Error('Invalid byte string'); return ensure0x(byteString.substring(2)); } + +export function addressToPostgresBytea(address: Address): string { + const hexString = addressToByteHexString(address); + return stringToPostgresBytea(hexString); +} + +export function postgresByteaToAddress( + byteString: string, + chainMetadata: ChainMetadata | null | undefined, +): Address { + const hexString = postgresByteaToString(byteString); + if (!chainMetadata) return hexString; + const addressBytes = Buffer.from(strip0x(hexString), 'hex'); + return bytesToProtocolAddress(addressBytes, chainMetadata.protocol, chainMetadata.bech32Prefix); +} + +export function searchValueToPostgresBytea(input: string): string { + if (isAddress(input)) return addressToPostgresBytea(input); + else return stringToPostgresBytea(input); +} diff --git a/src/features/messages/queries/parse.ts b/src/features/messages/queries/parse.ts index da05d52..487ba5b 100644 --- a/src/features/messages/queries/parse.ts +++ b/src/features/messages/queries/parse.ts @@ -6,7 +6,7 @@ import { tryUtf8DecodeBytes } from '../../../utils/string'; import { DomainsEntry } from '../../chains/queries/fragments'; import { isPiChain } from '../../chains/utils'; -import { postgresByteaToString } from './encoding'; +import { postgresByteaToAddress, postgresByteaToString } from './encoding'; import { MessageEntry, MessageStubEntry, @@ -53,12 +53,14 @@ function parseMessageStub( m: MessageStubEntry, ): MessageStub | null { try { - const destinationDomainId = m.destination_domain_id; - let destinationChainId = - m.destination_chain_id || multiProvider.tryGetChainId(destinationDomainId); + const originMetadata = multiProvider.tryGetChainMetadata(m.origin_domain_id); + const destinationMetadata = multiProvider.tryGetChainMetadata(m.destination_domain_id); + let destinationChainId = m.destination_chain_id || destinationMetadata?.chainId; if (!destinationChainId) { - logger.debug(`No chainId known for domain ${destinationDomainId}. Using domain as chainId`); - destinationChainId = destinationDomainId; + logger.debug( + `No chainId known for domain ${m.destination_domain_id}. Using domain as chainId`, + ); + destinationChainId = m.destination_domain_id; } const isPiMsg = isPiChain(multiProvider, scrapedChains, m.origin_chain_id) || @@ -69,22 +71,22 @@ function parseMessageStub( id: m.id.toString(), msgId: postgresByteaToString(m.msg_id), nonce: m.nonce, - sender: postgresByteaToString(m.sender), - recipient: postgresByteaToString(m.recipient), + sender: postgresByteaToAddress(m.sender, originMetadata), + recipient: postgresByteaToAddress(m.recipient, destinationMetadata), originChainId: m.origin_chain_id, originDomainId: m.origin_domain_id, destinationChainId, - destinationDomainId, + destinationDomainId: m.destination_domain_id, origin: { timestamp: parseTimestampString(m.send_occurred_at), hash: postgresByteaToString(m.origin_tx_hash), - from: postgresByteaToString(m.origin_tx_sender), + from: postgresByteaToAddress(m.origin_tx_sender, originMetadata), }, destination: m.is_delivered ? { timestamp: parseTimestampString(m.delivery_occurred_at!), hash: postgresByteaToString(m.destination_tx_hash!), - from: postgresByteaToString(m.destination_tx_sender!), + from: postgresByteaToAddress(m.destination_tx_sender!, destinationMetadata), } : undefined, isPiMsg, @@ -104,6 +106,9 @@ function parseMessage( const stub = parseMessageStub(multiProvider, scrapedChains, m); if (!stub) throw new Error('Message stub required'); + const originMetadata = multiProvider.tryGetChainMetadata(m.origin_domain_id); + const destinationMetadata = multiProvider.tryGetChainMetadata(m.destination_domain_id); + const body = postgresByteaToString(m.message_body ?? ''); const decodedBody = tryUtf8DecodeBytes(body); @@ -115,9 +120,9 @@ function parseMessage( ...stub.origin, blockHash: postgresByteaToString(m.origin_block_hash), blockNumber: m.origin_block_height, - mailbox: postgresByteaToString(m.origin_mailbox), + mailbox: postgresByteaToAddress(m.origin_mailbox, originMetadata), nonce: m.origin_tx_nonce, - to: postgresByteaToString(m.origin_tx_recipient), + to: postgresByteaToAddress(m.origin_tx_recipient, originMetadata), gasLimit: m.origin_tx_gas_limit, gasPrice: m.origin_tx_gas_price, effectiveGasPrice: m.origin_tx_effective_gas_price, @@ -131,9 +136,9 @@ function parseMessage( ...stub.destination, blockHash: postgresByteaToString(m.destination_block_hash!), blockNumber: m.destination_block_height!, - mailbox: postgresByteaToString(m.destination_mailbox!), + mailbox: postgresByteaToAddress(m.destination_mailbox!, destinationMetadata), nonce: m.destination_tx_nonce!, - to: postgresByteaToString(m.destination_tx_recipient!), + to: postgresByteaToAddress(m.destination_tx_recipient!, destinationMetadata), gasLimit: m.destination_tx_gas_limit!, gasPrice: m.destination_tx_gas_price!, effectiveGasPrice: m.destination_tx_effective_gas_price!, diff --git a/src/features/messages/queries/useMessageQuery.ts b/src/features/messages/queries/useMessageQuery.ts index 1e4172e..08e26b7 100644 --- a/src/features/messages/queries/useMessageQuery.ts +++ b/src/features/messages/queries/useMessageQuery.ts @@ -1,12 +1,16 @@ import { useCallback, useMemo } from 'react'; import { useQuery } from 'urql'; -import { isAddressEvm, isValidTransactionHashEvm } from '@hyperlane-xyz/utils'; +import { + isAddress, + isValidTransactionHashCosmos, + isValidTransactionHashEvm, +} from '@hyperlane-xyz/utils'; import { useMultiProvider } from '../../../store'; import { MessageStatus } from '../../../types'; import { useInterval } from '../../../utils/useInterval'; -import { useScrapedChains } from '../../chains/queries/useScrapedChains'; +import { useScrapedDomains } from '../../chains/queries/useScrapedChains'; import { MessageIdentifierType, buildMessageQuery, buildMessageSearchQuery } from './build'; import { MessagesQueryResult, MessagesStubQueryResult } from './fragments'; @@ -19,8 +23,11 @@ const SEARCH_QUERY_LIMIT = 50; export function isValidSearchQuery(input: string, allowAddress?: boolean) { if (!input) return false; - if (isValidTransactionHashEvm(input)) return true; - return !!(allowAddress && isAddressEvm(input)); + return !!( + isValidTransactionHashEvm(input) || + isValidTransactionHashCosmos(input) || + (allowAddress && isAddress(input)) + ); } export function useMessageSearchQuery( @@ -30,7 +37,7 @@ export function useMessageSearchQuery( startTimeFilter: number | null, endTimeFilter: number | null, ) { - const { scrapedChains } = useScrapedChains(); + const { scrapedDomains: scrapedChains } = useScrapedDomains(); const hasInput = !!sanitizedInput; const isValidInput = hasInput ? isValidSearchQuery(sanitizedInput, true) : true; @@ -80,7 +87,7 @@ export function useMessageSearchQuery( } export function useMessageQuery({ messageId, pause }: { messageId: string; pause: boolean }) { - const { scrapedChains } = useScrapedChains(); + const { scrapedDomains: scrapedChains } = useScrapedDomains(); // Assemble GraphQL Query const { query, variables } = buildMessageQuery(MessageIdentifierType.Id, messageId, 1); diff --git a/src/store.ts b/src/store.ts index 0f996da..d693566 100644 --- a/src/store.ts +++ b/src/store.ts @@ -15,8 +15,8 @@ const PERSIST_STATE_VERSION = 2; // Keeping everything here for now as state is simple // Will refactor into slices as necessary interface AppState { - scrapedChains: Array; - setScrapedChains: (chains: Array) => void; + scrapedDomains: Array; + setScrapedDomains: (chains: Array) => void; chainMetadata: ChainMap; setChainMetadata: (metadata: ChainMap) => void; chainMetadataOverrides: ChainMap>; @@ -32,8 +32,8 @@ interface AppState { export const useStore = create()( persist( (set, get) => ({ - scrapedChains: [], - setScrapedChains: (chains: Array) => set({ scrapedChains: chains }), + scrapedDomains: [], + setScrapedDomains: (domains: Array) => set({ scrapedDomains: domains }), chainMetadata: {}, setChainMetadata: (metadata: ChainMap) => set({ chainMetadata: metadata }), chainMetadataOverrides: {},