Fixes for Cosmos support (#115)

pull/116/head
J M Rossy 2 months ago committed by GitHub
parent f02dd1b75f
commit 3d0674decf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      src/components/search/SearchFilterBar.tsx
  2. 2
      src/components/search/SearchStates.tsx
  3. 4
      src/features/chains/ChainSearchModal.tsx
  4. 34
      src/features/chains/queries/useScrapedChains.ts
  5. 54
      src/features/messages/cards/TransactionCard.tsx
  6. 4
      src/features/messages/pi-queries/usePiChainMessageQuery.ts
  7. 12
      src/features/messages/queries/build.ts
  8. 33
      src/features/messages/queries/encoding.ts
  9. 35
      src/features/messages/queries/parse.ts
  10. 19
      src/features/messages/queries/useMessageQuery.ts
  11. 8
      src/store.ts

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

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

@ -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,

@ -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<DomainsEntry> }>({
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 };
}

@ -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({
<ChainSearchModal isOpen={isOpen} close={close} showAddChainMenu={true} />
</>
);
} else if (status === MessageStatus.Pending) {
if (isDestinationEvmChain) {
content = (
<DeliveryStatus>
<div className="flex flex-col items-center">
<div>Delivery to destination chain still in progress.</div>
{isPiMsg && (
<div className="mt-2 text-sm max-w-xs">
Please ensure a relayer is running for this chain.
</div>
)}
<Spinner classes="my-4 scale-75" />
<CallDataModal debugResult={debugResult} />
</div>
</DeliveryStatus>
);
} else {
content = (
<DeliveryStatus>
<div>Sorry, delivery information is currently available for only EVM-type chains.</div>
<div className="mt-2 text-sm pb-4">Support for other protocols is coming soon.</div>
</DeliveryStatus>
);
}
} else if (status === MessageStatus.Pending && isDestinationEvmChain) {
content = (
<DeliveryStatus>
<div className="flex flex-col items-center">
<div>Delivery to destination chain still in progress.</div>
{isPiMsg && (
<div className="mt-2 text-sm max-w-xs">
Please ensure a relayer is running for this chain.
</div>
)}
<Spinner classes="my-4 scale-75" />
<CallDataModal debugResult={debugResult} />
</div>
</DeliveryStatus>
);
} else {
content = (
<DeliveryStatus>
<div>{`Delivery to status is currently unknown. ${
isPiMsg
<div>Delivery to status is currently unknown.</div>
<div className="mt-2 text-sm pb-4">
{isPiMsg
? 'Please ensure your chain config is correct and check back later.'
: 'Please check again later'
}`}</div>
: 'Please check again later'}
</div>
</DeliveryStatus>
);
}
@ -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 (

@ -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();

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

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

@ -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!,

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

@ -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<DomainsEntry>;
setScrapedChains: (chains: Array<DomainsEntry>) => void;
scrapedDomains: Array<DomainsEntry>;
setScrapedDomains: (chains: Array<DomainsEntry>) => void;
chainMetadata: ChainMap<ChainMetadata>;
setChainMetadata: (metadata: ChainMap<ChainMetadata>) => void;
chainMetadataOverrides: ChainMap<Partial<ChainMetadata>>;
@ -32,8 +32,8 @@ interface AppState {
export const useStore = create<AppState>()(
persist(
(set, get) => ({
scrapedChains: [],
setScrapedChains: (chains: Array<DomainsEntry>) => set({ scrapedChains: chains }),
scrapedDomains: [],
setScrapedDomains: (domains: Array<DomainsEntry>) => set({ scrapedDomains: domains }),
chainMetadata: {},
setChainMetadata: (metadata: ChainMap<ChainMetadata>) => set({ chainMetadata: metadata }),
chainMetadataOverrides: {},

Loading…
Cancel
Save