Use new chain widget props

Replace chainConfig store state with chainMetadataOverrides
pull/114/head
J M Rossy 2 months ago
parent ae86d66061
commit eabdf92263
  1. 14
      src/components/nav/Header.tsx
  2. 23
      src/components/search/SearchFilterBar.tsx
  3. 2
      src/components/search/SearchStates.tsx
  4. 15
      src/features/api/getMessages.ts
  5. 11
      src/features/api/getStatus.ts
  6. 15
      src/features/api/searchMessages.ts
  7. 25
      src/features/api/searchPiMessages.ts
  8. 10
      src/features/api/types.ts
  9. 8
      src/features/api/utils.ts
  10. 2
      src/features/chains/ChainConfigSyncer.tsx
  11. 182
      src/features/chains/ConfigureChains.tsx
  12. 96
      src/features/chains/chainConfig.ts
  13. 32
      src/features/chains/chainconfig.test.ts
  14. 3
      src/features/chains/queries/useScrapedChains.ts
  15. 47
      src/features/chains/useChainMetadata.ts
  16. 7
      src/features/chains/utils.ts
  17. 7
      src/features/debugger/debugMessage.ts
  18. 9
      src/features/deliveryStatus/fetchDeliveryStatus.ts
  19. 7
      src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  20. 10
      src/features/messages/pi-queries/fetchPiChainMessages.test.ts
  21. 45
      src/features/messages/pi-queries/fetchPiChainMessages.ts
  22. 13
      src/features/messages/pi-queries/usePiChainMessageQuery.ts
  23. 3
      src/images/backgrounds/footer-line-desktop.svg
  24. 3
      src/images/backgrounds/footer-line-mobile.svg
  25. 0
      src/multiProvider.ts
  26. 13
      src/pages/settings.tsx
  27. 48
      src/store.ts

@ -63,9 +63,9 @@ export function Header({ pathName }: { pathName: string }) {
>
Docs
</a>
<Link href="/settings" className={navLinkClass('/settings')}>
{/* <Link href="/settings" className={navLinkClass('/settings')}>
Settings
</Link>
</Link> */}
{showSearch && <MiniSearchBar />}
</nav>
{/* Dropdown menu, used on mobile */}
@ -79,11 +79,11 @@ export function Header({ pathName }: { pathName: string }) {
Home
</MobileNavLink>
),
({ close }) => (
<MobileNavLink href="/settings" closeDropdown={close} key="Settings">
Settings
</MobileNavLink>
),
// ({ close }) => (
// <MobileNavLink href="/settings" closeDropdown={close} key="Settings">
// Settings
// </MobileNavLink>
// ),
// ({ close }) => (
// <MobileNavLink href="/api" closeDropdown={c} key="API">
// API

@ -1,5 +1,4 @@
import clsx from 'clsx';
import Link from 'next/link';
import { useState } from 'react';
import { ChainMetadata } from '@hyperlane-xyz/sdk';
@ -7,7 +6,6 @@ import { trimToLength } from '@hyperlane-xyz/utils';
import {
ChainSearchMenu,
ChevronIcon,
GearIcon,
IconButton,
Modal,
Popover,
@ -16,7 +14,7 @@ import {
import { useScrapedEvmChains } from '../../features/chains/queries/useScrapedChains';
import { getChainDisplayName } from '../../features/chains/utils';
import { useMultiProvider } from '../../store';
import { useMultiProvider, useStore } from '../../store';
import { Color } from '../../styles/Color';
import { SolidButton } from '../buttons/SolidButton';
import { TextButton } from '../buttons/TextButton';
@ -57,11 +55,6 @@ export function SearchFilterBar({
endValue={endTimestamp}
onChangeEndValue={onChangeEndTimestamp}
/>
<Link href="/settings" title="View explorer settings" className="hidden sm:block">
<div className="active:opacity-90 hover:rotate-90 transition-all">
<GearIcon color={Color.pink} height={18} width={18} />
</div>
</Link>
</div>
);
}
@ -77,6 +70,10 @@ function ChainSelector({
}) {
const multiProvider = useMultiProvider();
const { chains } = useScrapedEvmChains(multiProvider);
const { chainMetadataOverrides, setChainMetadataOverrides } = useStore((s) => ({
chainMetadataOverrides: s.chainMetadataOverrides,
setChainMetadataOverrides: s.setChainMetadataOverrides,
}));
const [showModal, setShowModal] = useState(false);
const closeModal = () => {
@ -121,9 +118,15 @@ function ChainSelector({
<Modal
isOpen={showModal}
close={closeModal}
panelClassname="p-4 sm:p-5 max-w-lg min-h-[50vh]"
panelClassname="p-4 sm:p-5 max-w-lg min-h-[40vh]"
>
<ChainSearchMenu chainMetadata={chains} onClickChain={onClickChain} />
<ChainSearchMenu
chainMetadata={chains}
onClickChain={onClickChain}
overrideChainMetadata={chainMetadataOverrides}
onChangeOverrideMetadata={setChainMetadataOverrides}
showAddChainButton={true}
/>
</Modal>
</div>
);

@ -18,7 +18,7 @@ export function SearchFetching({ show, isPiFetching }: { show: boolean; isPiFetc
<Spinner />
</div>
<div className="mt-4 text-center font-light leading-loose text-gray-700">
{isPiFetching ? 'Searching custom chains for messages' : 'Searching for messages'}
{isPiFetching ? 'Searching override chains for messages' : 'Searching for messages'}
</div>
</div>
</div>

@ -1,6 +1,8 @@
import { Client } from '@urql/core';
import type { NextApiRequest } from 'next';
import { Result, failure, success } from '@hyperlane-xyz/utils';
import { API_GRAPHQL_QUERY_LIMIT } from '../../consts/api';
import { logger } from '../../utils/logger';
import { sanitizeString } from '../../utils/string';
@ -8,15 +10,12 @@ import { MessageIdentifierType, buildMessageQuery } from '../messages/queries/bu
import { MessagesQueryResult } from '../messages/queries/fragments';
import { parseMessageQueryResult } from '../messages/queries/parse';
import { ApiHandlerResult, ApiMessage, toApiMessage } from './types';
import { failureResult, getMultiProvider, getScrapedChains, successResult } from './utils';
import { ApiMessage, toApiMessage } from './types';
import { getMultiProvider, getScrapedChains } from './utils';
export async function handler(
req: NextApiRequest,
client: Client,
): Promise<ApiHandlerResult<ApiMessage[]>> {
export async function handler(req: NextApiRequest, client: Client): Promise<Result<ApiMessage[]>> {
const identifierParam = parseQueryParams(req);
if (!identifierParam) return failureResult('No message identifier param provided');
if (!identifierParam) return failure('No message identifier param provided');
logger.debug('Attempting to find messages matching:', identifierParam);
const { query, variables } = buildMessageQuery(
@ -30,7 +29,7 @@ export async function handler(
const scrapedChains = await getScrapedChains(client);
const messages = parseMessageQueryResult(multiProvider, scrapedChains, result.data);
return successResult(messages.map(toApiMessage));
return success(messages.map(toApiMessage));
}
// TODO replace with Zod

@ -1,6 +1,8 @@
import { Client } from '@urql/core';
import type { NextApiRequest } from 'next';
import { Result, failure, success } from '@hyperlane-xyz/utils';
import { API_GRAPHQL_QUERY_LIMIT } from '../../consts/api';
import { MessageStatus } from '../../types';
import { logger } from '../../utils/logger';
@ -9,8 +11,7 @@ import { MessagesStubQueryResult } from '../messages/queries/fragments';
import { parseMessageStubResult } from '../messages/queries/parse';
import { parseQueryParams } from './getMessages';
import { ApiHandlerResult } from './types';
import { failureResult, getMultiProvider, getScrapedChains, successResult } from './utils';
import { getMultiProvider, getScrapedChains } from './utils';
interface MessageStatusResult {
id: string;
@ -20,9 +21,9 @@ interface MessageStatusResult {
export async function handler(
req: NextApiRequest,
client: Client,
): Promise<ApiHandlerResult<MessageStatusResult[]>> {
): Promise<Result<MessageStatusResult[]>> {
const identifierParam = parseQueryParams(req);
if (!identifierParam) return failureResult('No message identifier param provided');
if (!identifierParam) return failure('No message identifier param provided');
logger.debug('Attempting to find message status matching:', identifierParam);
const { query, variables } = buildMessageQuery(
@ -38,5 +39,5 @@ export async function handler(
const messages = parseMessageStubResult(multiProvider, scrapedChains, result.data);
return successResult(messages.map((m) => ({ id: m.msgId, status: m.status })));
return success(messages.map((m) => ({ id: m.msgId, status: m.status })));
}

@ -1,6 +1,8 @@
import { Client } from '@urql/core';
import type { NextApiRequest } from 'next';
import { Result, failure, success } from '@hyperlane-xyz/utils';
import { API_GRAPHQL_QUERY_LIMIT } from '../../consts/api';
import { logger } from '../../utils/logger';
import { sanitizeString } from '../../utils/string';
@ -8,17 +10,14 @@ import { buildMessageSearchQuery } from '../messages/queries/build';
import { MessagesQueryResult } from '../messages/queries/fragments';
import { parseMessageQueryResult } from '../messages/queries/parse';
import { ApiHandlerResult, ApiMessage, toApiMessage } from './types';
import { failureResult, getMultiProvider, getScrapedChains, successResult } from './utils';
import { ApiMessage, toApiMessage } from './types';
import { getMultiProvider, getScrapedChains } from './utils';
const SEARCH_QUERY_PARAM_NAME = 'query';
export async function handler(
req: NextApiRequest,
client: Client,
): Promise<ApiHandlerResult<ApiMessage[]>> {
export async function handler(req: NextApiRequest, client: Client): Promise<Result<ApiMessage[]>> {
const queryValue = parseSearchQueryParam(req);
if (!queryValue) return failureResult('No query param provided');
if (!queryValue) return failure('No query param provided');
logger.debug('Attempting to search for messages:', queryValue);
// TODO consider supporting time/chain filters here
@ -37,7 +36,7 @@ export async function handler(
const messages = parseMessageQueryResult(multiProvider, scrapedChains, result.data);
return successResult(messages.map(toApiMessage));
return success(messages.map(toApiMessage));
}
// TODO replace with Zod

@ -2,18 +2,17 @@ import type { NextApiRequest } from 'next';
import { z } from 'zod';
import { GithubRegistry } from '@hyperlane-xyz/registry';
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { ChainMetadataSchema, MultiProvider } from '@hyperlane-xyz/sdk';
import { Result, failure, success } from '@hyperlane-xyz/utils';
import { config } from '../../consts/config';
import { logger } from '../../utils/logger';
import { tryParseChainConfig } from '../chains/chainConfig';
import {
PiMessageQuery,
fetchMessagesFromPiChain,
} from '../messages/pi-queries/fetchPiChainMessages';
import { ApiHandlerResult, ApiMessage } from './types';
import { failureResult, successResult } from './utils';
import { ApiMessage } from './types';
const queryParamSchema = z.object({
query: z.string(),
@ -21,25 +20,25 @@ const queryParamSchema = z.object({
toBlock: z.string().optional(),
});
export async function handler(req: NextApiRequest): Promise<ApiHandlerResult<ApiMessage[]>> {
export async function handler(req: NextApiRequest): Promise<Result<ApiMessage[]>> {
const query = tryParseParams(req);
if (!query) return failureResult('Invalid query params provided');
if (!query) return failure('Invalid query params provided');
const parseResult = tryParseChainConfig(req.body);
if (!parseResult.success) return failureResult(`Invalid chain configs: ${parseResult.error}`);
const chainConfig = parseResult.chainConfig;
const parseResult = ChainMetadataSchema.safeParse(req.body);
if (!parseResult.success) return failure(`Invalid chain configs: ${parseResult.error}`);
const chainMetadata = parseResult.data;
try {
logger.debug('Attempting to search for PI messages:', query);
const multiProvider = new MultiProvider({ [chainConfig.name]: chainConfig });
const multiProvider = new MultiProvider({ [chainMetadata.name]: chainMetadata });
const registry = new GithubRegistry({ proxyUrl: config.githubProxy });
// TODO consider supporting block/time/chain filters here
const messages = await fetchMessagesFromPiChain(chainConfig, query, multiProvider, registry);
const messages = await fetchMessagesFromPiChain(chainMetadata, query, multiProvider, registry);
logger.debug(`Found ${messages.length} PI messages`);
return successResult(messages);
return success(messages);
} catch (error) {
logger.error('Error fetching PI messages', error);
return failureResult('Unable to fetch messages, check config and query');
return failure('Unable to fetch messages, check config and query');
}
}

@ -1,15 +1,5 @@
import { Message } from '../../types';
export type ApiHandlerResult<T> =
| {
success: true;
data: T;
}
| {
success: false;
error: string;
};
export type ApiMessage = Omit<
Message,
| 'msgId' // use id field for msgId

@ -7,14 +7,6 @@ import { config } from '../../consts/config';
import { logger } from '../../utils/logger';
import { DOMAINS_QUERY, DomainsEntry } from '../chains/queries/fragments';
export function successResult<R>(data: R): { success: true; data: R } {
return { success: true, data };
}
export function failureResult(error: string): { success: false; error: string } {
return { success: false, error };
}
// TODO de-dupe this with store.ts and handle registry/multiProvider concerns in a single place
export async function getMultiProvider(): Promise<MultiProvider> {
const registry = new GithubRegistry({ proxyUrl: config.githubProxy });

@ -1,6 +1,6 @@
import { PropsWithChildren } from 'react';
import { useQueryParamChainConfigSync } from './useChainConfigs';
import { useQueryParamChainConfigSync } from './useChainMetadata';
export function ChainConfigSyncer({ children }: PropsWithChildren<Record<never, any>>) {
useQueryParamChainConfigSync();

@ -1,182 +0,0 @@
import { ChangeEventHandler, useState } from 'react';
import { ChainName } from '@hyperlane-xyz/sdk';
import { IconButton, Modal, XIcon } from '@hyperlane-xyz/widgets';
import { CopyButton } from '../../components/buttons/CopyButton';
import { SolidButton } from '../../components/buttons/SolidButton';
import { ChainLogo } from '../../components/icons/ChainLogo';
import { Card } from '../../components/layout/Card';
import { docLinks } from '../../consts/links';
import { useMultiProvider } from '../../store';
import { tryParseChainConfig } from './chainConfig';
import { useChainConfigsRW } from './useChainConfigs';
export function ConfigureChains() {
const { chainConfigs, setChainConfigs } = useChainConfigsRW();
const multiProvider = useMultiProvider();
const [showAddChainModal, setShowAddChainModal] = useState(false);
const [customChainInput, setCustomChainInput] = useState('');
const onCustomChainInputChange: ChangeEventHandler<HTMLTextAreaElement> = (e) => {
setCustomChainInput(e?.target?.value || '');
};
const [chainInputErr, setChainInputErr] = useState('');
const closeModal = () => {
setShowAddChainModal(false);
setChainInputErr('');
};
const onClickAddChain = () => {
setChainInputErr('');
const result = tryParseChainConfig(customChainInput, multiProvider);
if (result.success) {
setChainConfigs({
...chainConfigs,
[result.chainConfig.name]: result.chainConfig,
});
setCustomChainInput('');
setShowAddChainModal(false);
} else {
setChainInputErr(`Invalid config: ${result.error}`);
}
};
const onClickRemoveChain = (chainName: ChainName) => {
const newChainConfigs = { ...chainConfigs };
delete newChainConfigs[chainName];
setChainConfigs({
...newChainConfigs,
});
};
return (
<Card>
<h2 className="mt-1 text-lg text-blue-500 font-medium">Chain Settings</h2>
<p className="mt-3 font-light">
Hyperlane can be deployed to any chain using{' '}
<a
href={docLinks.pi}
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 text-blue-500 hover:text-blue-400"
>
Permissionless Interoperability (PI)
</a>
. This explorer can be configured to search for messages on any PI chain.
</p>
<p className="mt-3 font-light">
To make your chain available to all users, add its metadata to the{' '}
<a
href={docLinks.registry}
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 text-blue-500 hover:text-blue-400"
>
canonical Hyperlane Registry
</a>
. Or use the section below to add it for just your own use.
</p>
<h3 className="mt-6 text-lg text-blue-500 font-medium">Custom Chains</h3>
<table className="mt-2 w-full">
<thead>
<tr>
<th className={styles.header}>Chain</th>
<th className={styles.header}>Chain ID</th>
<th className={styles.header}>Domain ID</th>
<th className={styles.header}>Name</th>
<th className={`${styles.header} hidden sm:table-cell`}>RPC URL</th>
<th className={`${styles.header} hidden md:table-cell`}>Explorer</th>
<th className={styles.header}></th>
</tr>
</thead>
<tbody>
{Object.values(chainConfigs).map((chain) => (
<tr key={`chain-${chain.chainId}`}>
<td>
<ChainLogo chainId={chain.chainId} size={32} background={true} />
</td>
<td className={styles.value}>{chain.chainId}</td>
<td className={styles.value}>{chain.domainId || chain.chainId}</td>
<td className={styles.value}>{chain.displayName || chain.name}</td>
<td className={styles.value + ' hidden sm:table-cell'}>
{chain.rpcUrls?.[0]?.http || 'Unknown'}
</td>
<td className={styles.value + ' hidden md:table-cell'}>
{chain.blockExplorers?.[0]?.url || 'Unknown'}
</td>
<td>
<IconButton onClick={() => onClickRemoveChain(chain.name)} title="Remove">
<XIcon width={10} height={10} />
</IconButton>
</td>
</tr>
))}
</tbody>
</table>
<SolidButton classes="mt-4 mb-2 py-0.5 w-full" onClick={() => setShowAddChainModal(true)}>
Add custom chain
</SolidButton>
<Modal isOpen={showAddChainModal} close={closeModal} panelClassname="max-w-lg p-4 sm:p-5">
<p className="mt-2 font-light">
Input a chain metadata config including core contract addresses to enable exploration of
that chain. See{' '}
<a
href={docLinks.pi}
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 text-blue-500 hover:text-blue-400"
>
PI Explorer documentation
</a>{' '}
for examples.
</p>
<div className="relative mt-4">
<textarea
className="w-full min-h-[20rem] p-2 border border-gray-400 rounded-xl text-sm font-light focus:outline-none"
placeholder={customChainTextareaPlaceholder}
value={customChainInput}
onChange={onCustomChainInputChange}
></textarea>
<CopyButton
copyValue={customChainInput || customChainTextareaPlaceholder}
width={16}
height={16}
classes="absolute top-3 right-3"
/>
</div>
{chainInputErr && <div className="mt-2 text-red-600 text-sm">{chainInputErr}</div>}
<SolidButton classes="mt-2 mb-2 py-0.5 w-full" onClick={onClickAddChain}>
Add
</SolidButton>
</Modal>
</Card>
);
}
const customChainTextareaPlaceholder = `---
chainId: 11155111
name: sepolia
protocol: ethereum
rpcUrls:
- http: https://foobar.com
blockExplorers:
- name: Sepolia Etherscan
family: etherscan
url: https://sepolia.etherscan.io
apiUrl: https://api-sepolia.etherscan.io/api
apiKey: '12345'
blocks:
confirmations: 1
estimateBlockTime: 13
mailbox: 0x123...
`;
const styles = {
header: 'pt-2 pb-1 text-sm text-gray-700 font-normal text-left',
value: 'py-4 px-1 text-sm font-light',
valueTruncated: 'py-4 text-sm font-light truncate',
};

@ -1,96 +0,0 @@
import { parse as yamlParse } from 'yaml';
import { z } from 'zod';
import { ChainMetadata, ChainMetadataSchemaObject, MultiProvider } from '@hyperlane-xyz/sdk';
import { logger } from '../../utils/logger';
export const ChainConfigSchema = ChainMetadataSchemaObject.extend({
mailbox: z.string().optional(),
interchainGasPaymaster: z.string().optional(),
});
export type ChainConfig = ChainMetadata & { mailbox?: Address; interchainGasPaymaster?: Address };
type ParseResult =
| {
success: true;
chainConfig: ChainConfig;
}
| {
success: false;
error: string;
};
export function tryParseChainConfig(input: string, mp?: MultiProvider): ParseResult {
let data: any;
try {
if (input.startsWith('{')) {
data = JSON.parse(input);
} else {
data = yamlParse(input);
}
} catch (error) {
logger.error('Error parsing chain config', error);
return {
success: false,
error: 'Input is not valid chain JSON or YAML',
};
}
const result = ChainConfigSchema.safeParse(data);
if (!result.success) {
logger.error('Error validating chain config', result.error);
const firstIssue = result.error.issues[0];
return {
success: false,
error: `${firstIssue.path} => ${firstIssue.message}`,
};
}
const chainConfig = result.data as ChainConfig;
// Ensure https is used for RPCs
const rpcUrls = chainConfig.rpcUrls;
if (rpcUrls?.some((r) => !r.http.startsWith('https://') && !r.http.includes('localhost'))) {
return {
success: false,
error: 'all RPCs must use valid https url',
};
}
// Force blockExplorers family value for now
const blockExplorers = chainConfig.blockExplorers;
if (blockExplorers?.some((e) => !e.family)) {
return {
success: false,
error: 'family field for block explorers must be "etherscan"',
};
}
// Reject blockscout explorers for now
if (blockExplorers?.[0]?.url.includes('blockscout')) {
return {
success: false,
error: 'only Etherscan-based explorers are supported at this time',
};
}
if (
mp &&
(mp.tryGetChainMetadata(chainConfig.name) ||
mp.tryGetChainMetadata(chainConfig.chainId) ||
(chainConfig.domainId && mp.tryGetChainMetadata(chainConfig.domainId)))
) {
return {
success: false,
error: 'chainId, domainId, or name is already in use',
};
}
return {
success: true,
chainConfig,
};
}

@ -1,32 +0,0 @@
import { ChainMetadata, ExplorerFamily } from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { tryParseChainConfig } from './chainConfig';
const validConfig: ChainMetadata<{ mailbox: Address }> = {
chainId: 12345,
name: 'mytestnet',
protocol: ProtocolType.Ethereum,
rpcUrls: [{ http: 'https://fakerpc.com' }],
blockExplorers: [
{
name: 'FakeScan',
family: ExplorerFamily.Other,
url: 'https://fakeexplorer.com',
apiUrl: 'https://fakeexplorer.com',
},
],
blocks: { confirmations: 1, estimateBlockTime: 10 },
mailbox: '0x14999bccB37118713891DAAA1D5959a02E206C1f',
};
describe('chain configs', () => {
it('parses valid config', async () => {
const result = tryParseChainConfig(JSON.stringify(validConfig));
expect(result.success).toBe(true);
});
it('rejects invalid config', async () => {
const result = tryParseChainConfig(JSON.stringify({ ...validConfig, chainId: undefined }));
expect(result.success).toBe(false);
});
});

@ -36,12 +36,13 @@ export function useScrapedChains() {
export function useScrapedEvmChains(multiProvider: MultiProvider) {
const { scrapedChains, isFetching, isError } = useScrapedChains();
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(
multiProvider.metadata,
chainMetadata,
(_, chainMetadata): chainMetadata is ChainMetadata =>
isEvmChain(multiProvider, chainMetadata.chainId) &&
!isPiChain(multiProvider, scrapedChains, chainMetadata.chainId) &&

@ -1,36 +1,29 @@
import { useEffect } from 'react';
import { z } from 'zod';
import { ChainMap, ChainMetadata, ChainMetadataSchema } from '@hyperlane-xyz/sdk';
import { fromBase64, objMerge } from '@hyperlane-xyz/utils';
import {
ChainMap,
ChainMetadata,
ChainMetadataSchema,
mergeChainMetadataMap,
} from '@hyperlane-xyz/sdk';
import { fromBase64 } from '@hyperlane-xyz/utils';
import { useStore } from '../../store';
import { logger } from '../../utils/logger';
import { useQueryParam } from '../../utils/queryParams';
import { ChainConfig } from './chainConfig';
const CHAIN_CONFIGS_KEY = 'chains';
const ChainMetadataArraySchema = z.array(ChainMetadataSchema);
// Use the chainConfigs from the store
export function useChainConfigs() {
return useStore((s) => s.chainConfigs);
}
// Use the chainConfigs and setChainConfigs from the store (i.e. Read/Write)
export function useChainConfigsRW() {
return useStore((s) => ({
chainConfigs: s.chainConfigs,
setChainConfigs: s.setChainConfigs,
}));
}
// Look for chainConfigs in the query string and merge them into the store
// Look for chainMetadata in the query string and merge them into the store
// Not to be used directly, should only require a single use in ChainConfigSyncer
export function useQueryParamChainConfigSync() {
const { chainConfigs: storeConfigs, setChainConfigs } = useChainConfigsRW();
const { chainMetadataOverrides, setChainMetadataOverrides } = useStore((s) => ({
chainMetadataOverrides: s.chainMetadataOverrides,
setChainMetadataOverrides: s.setChainMetadataOverrides,
}));
const queryVal = useQueryParam(CHAIN_CONFIGS_KEY);
useEffect(() => {
@ -48,9 +41,13 @@ export function useQueryParamChainConfigSync() {
const chainMetadataList = result.data as ChainMetadata[];
// Stop here if there are no new configs to save, otherwise the effect will loop
if (!chainMetadataList.length || chainMetadataList.every((c) => !!storeConfigs[c.name])) return;
if (
!chainMetadataList.length ||
chainMetadataList.every((c) => !!chainMetadataOverrides[c.name])
)
return;
const nameToChainConfig = chainMetadataList.reduce<ChainMap<ChainConfig>>(
const nameToChainConfig = chainMetadataList.reduce<ChainMap<ChainMetadata>>(
(acc, chainMetadata) => {
// TODO would be great if we could get contract addrs here too
// But would require apps like warp template to get that from devs
@ -60,9 +57,9 @@ export function useQueryParamChainConfigSync() {
{},
);
const mergedConfig = objMerge(nameToChainConfig, storeConfigs) as ChainMap<ChainConfig>;
setChainConfigs(mergedConfig);
}, [storeConfigs, setChainConfigs, queryVal]);
const mergedConfig = mergeChainMetadataMap(nameToChainConfig, chainMetadataOverrides);
setChainMetadataOverrides(mergedConfig);
}, [chainMetadataOverrides, setChainMetadataOverrides, queryVal]);
return storeConfigs;
return chainMetadataOverrides;
}

@ -1,18 +1,17 @@
import { IRegistry } from '@hyperlane-xyz/registry';
import { ChainMap, MultiProvider } from '@hyperlane-xyz/sdk';
import { ChainMap, ChainMetadata, MultiProvider } from '@hyperlane-xyz/sdk';
import { ProtocolType, toTitleCase } from '@hyperlane-xyz/utils';
import { Environment } from '../../consts/environments';
import { ChainConfig } from './chainConfig';
import { DomainsEntry } from './queries/fragments';
export async function getMailboxAddress(
chainName: string,
customChainConfigs: ChainMap<ChainConfig>,
overrideChainMetadata: ChainMap<Partial<ChainMetadata<{ mailbox?: string }>>>,
registry: IRegistry,
) {
if (customChainConfigs[chainName]?.mailbox) return customChainConfigs[chainName].mailbox;
if (overrideChainMetadata[chainName]?.mailbox) return overrideChainMetadata[chainName].mailbox;
const addresses = await registry.getChainAddresses(chainName);
if (addresses?.mailbox) return addresses.mailbox;
else return undefined;

@ -10,7 +10,7 @@ import {
InterchainGasPaymaster__factory,
} from '@hyperlane-xyz/core';
import { IRegistry } from '@hyperlane-xyz/registry';
import { ChainMap, MAILBOX_VERSION, MultiProvider } from '@hyperlane-xyz/sdk';
import { ChainMap, ChainMetadata, MAILBOX_VERSION, MultiProvider } from '@hyperlane-xyz/sdk';
import {
addressToBytes32,
errorToString,
@ -22,7 +22,6 @@ import {
import { Message } from '../../types';
import { logger } from '../../utils/logger';
import type { ChainConfig } from '../chains/chainConfig';
import { getMailboxAddress } from '../chains/utils';
import { isIcaMessage, tryDecodeIcaBody, tryFetchIcaAddress } from '../messages/ica';
@ -36,7 +35,7 @@ const IGP_PAYMENT_CHECK_DELAY = 30_000; // 30 seconds
export async function debugMessage(
multiProvider: MultiProvider,
registry: IRegistry,
customChainConfigs: ChainMap<ChainConfig>,
overrideChainMetadata: ChainMap<Partial<ChainMetadata>>,
{
msgId,
nonce,
@ -73,7 +72,7 @@ export async function debugMessage(
const recipInvalid = await isInvalidRecipient(destProvider, recipient);
if (recipInvalid) return recipInvalid;
const destMailbox = await getMailboxAddress(destName, customChainConfigs, registry);
const destMailbox = await getMailboxAddress(destName, overrideChainMetadata, registry);
if (!destMailbox)
throw new Error(`Cannot debug message, no mailbox address provided for chain ${destName}`);

@ -2,13 +2,12 @@ import { constants } from 'ethers';
import { IMailbox__factory } from '@hyperlane-xyz/core';
import { IRegistry } from '@hyperlane-xyz/registry';
import { ChainMap, MultiProvider } from '@hyperlane-xyz/sdk';
import { ChainMap, ChainMetadata, MultiProvider } from '@hyperlane-xyz/sdk';
import { DELIVERY_LOG_CHECK_BLOCK_RANGE } from '../../consts/values';
import { Message, MessageStatus } from '../../types';
import { logger } from '../../utils/logger';
import { toDecimalNumber } from '../../utils/number';
import type { ChainConfig } from '../chains/chainConfig';
import { getMailboxAddress } from '../chains/utils';
import { debugMessage } from '../debugger/debugMessage';
import { MessageDebugStatus } from '../debugger/types';
@ -23,11 +22,11 @@ import {
export async function fetchDeliveryStatus(
multiProvider: MultiProvider,
registry: IRegistry,
customChainConfigs: ChainMap<ChainConfig>,
overrideChainMetadata: ChainMap<Partial<ChainMetadata>>,
message: Message,
): Promise<MessageDeliveryStatusResponse> {
const destName = multiProvider.getChainName(message.destinationChainId);
const destMailboxAddr = await getMailboxAddress(destName, customChainConfigs, registry);
const destMailboxAddr = await getMailboxAddress(destName, overrideChainMetadata, registry);
if (!destMailboxAddr)
throw new Error(
`Cannot check delivery status, no mailbox address provided for chain ${destName}`,
@ -68,7 +67,7 @@ export async function fetchDeliveryStatus(
};
return result;
} else {
const debugResult = await debugMessage(multiProvider, registry, customChainConfigs, message);
const debugResult = await debugMessage(multiProvider, registry, overrideChainMetadata, message);
const messageStatus =
debugResult.status === MessageDebugStatus.NoErrorsFound
? MessageStatus.Pending

@ -5,11 +5,10 @@ import { toast } from 'react-toastify';
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { errorToString } from '@hyperlane-xyz/utils';
import { useReadyMultiProvider, useRegistry } from '../../store';
import { useReadyMultiProvider, useRegistry, useStore } from '../../store';
import { Message, MessageStatus } from '../../types';
import { logger } from '../../utils/logger';
import { MissingChainConfigToast } from '../chains/MissingChainConfigToast';
import { useChainConfigs } from '../chains/useChainConfigs';
import { isEvmChain } from '../chains/utils';
import { fetchDeliveryStatus } from './fetchDeliveryStatus';
@ -21,7 +20,7 @@ export function useMessageDeliveryStatus({
message: Message;
enabled: boolean;
}) {
const chainConfigs = useChainConfigs();
const chainMetadataOverrides = useStore((s) => s.chainMetadataOverrides) || {};
const multiProvider = useReadyMultiProvider();
const registry = useRegistry();
@ -46,7 +45,7 @@ export function useMessageDeliveryStatus({
const deliverStatus = await fetchDeliveryStatus(
multiProvider,
registry,
chainConfigs,
chainMetadataOverrides,
message,
);

@ -1,9 +1,8 @@
import { GithubRegistry, chainAddresses, chainMetadata } from '@hyperlane-xyz/registry';
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { ChainMetadata, MultiProvider } from '@hyperlane-xyz/sdk';
import { config } from '../../../consts/config';
import { Message, MessageStatus } from '../../../types';
import { ChainConfig } from '../../chains/chainConfig';
import { fetchMessagesFromPiChain } from './fetchPiChainMessages';
@ -15,7 +14,10 @@ jest.setTimeout(30000);
const sepoliaMailbox = chainAddresses.sepolia.mailbox;
const sepoliaIgp = chainAddresses.sepolia.interchainGasPaymaster;
const sepoliaConfigWithExplorer: ChainConfig = {
const sepoliaConfigWithExplorer: ChainMetadata<{
mailbox: string;
interchainGasPaymaster: string;
}> = {
...chainMetadata.sepolia,
mailbox: sepoliaMailbox,
interchainGasPaymaster: sepoliaIgp,
@ -165,6 +167,6 @@ describe('fetchMessagesFromPiChain', () => {
});
});
function createMP(config: ChainConfig) {
function createMP(config: ChainMetadata) {
return new MultiProvider({ ...chainMetadata, sepolia: config });
}

@ -2,7 +2,7 @@ import { BigNumber, constants, ethers, providers } from 'ethers';
import { IInterchainGasPaymaster__factory, Mailbox__factory } from '@hyperlane-xyz/core';
import { IRegistry } from '@hyperlane-xyz/registry';
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { ChainMetadata, MultiProvider } from '@hyperlane-xyz/sdk';
import {
ProtocolType,
addressToBytes32,
@ -17,7 +17,6 @@ import {
import { PI_MESSAGE_LOG_CHECK_BLOCK_RANGE } from '../../../consts/values';
import { ExtendedLog, Message, MessageStatus } from '../../../types';
import { logger } from '../../../utils/logger';
import { ChainConfig } from '../../chains/chainConfig';
const mailbox = Mailbox__factory.createInterface();
const dispatchTopic0 = mailbox.getEventTopic('Dispatch');
@ -65,7 +64,7 @@ searchForMessages(input):
*/
export async function fetchMessagesFromPiChain(
chainConfig: ChainConfig,
chainMetadata: ChainMetadata,
query: PiMessageQuery,
multiProvider: MultiProvider,
registry: IRegistry,
@ -75,14 +74,14 @@ export async function fetchMessagesFromPiChain(
let logs: ExtendedLog[] = [];
if (isValidAddress(input) && (!queryType || queryType === PiQueryType.Address)) {
logs = await fetchLogsForAddress(chainConfig, query, multiProvider, registry);
logs = await fetchLogsForAddress(chainMetadata, query, multiProvider, registry);
} else if (isValidTransactionHash(input, ProtocolType.Ethereum)) {
if (!queryType || queryType === PiQueryType.TxHash) {
logs = await fetchLogsForTxHash(chainConfig, query, multiProvider);
logs = await fetchLogsForTxHash(chainMetadata, query, multiProvider);
}
// Input may be a msg id, check that next
if ((!queryType || queryType === PiQueryType.MsgId) && !logs.length) {
logs = await fetchLogsForMsgId(chainConfig, query, multiProvider, registry);
logs = await fetchLogsForMsgId(chainMetadata, query, multiProvider, registry);
}
} else {
logger.warn('Invalid PI search input', input, queryType);
@ -90,7 +89,7 @@ export async function fetchMessagesFromPiChain(
}
const messages = logs
.map((l) => logToMessage(multiProvider, l, chainConfig))
.map((l) => logToMessage(multiProvider, l, chainMetadata))
.filter((m): m is Message => !!m);
// Fetch IGP gas payments for each message if it's a small set
@ -98,7 +97,7 @@ export async function fetchMessagesFromPiChain(
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));
messagesWithGasPayments.push(await tryFetchIgpGasPayments(m, chainMetadata, multiProvider));
}
return messagesWithGasPayments;
} else {
@ -108,15 +107,15 @@ export async function fetchMessagesFromPiChain(
}
async function fetchLogsForAddress(
chainConfig: ChainConfig,
chainMetadata: ChainMetadata,
query: PiMessageQuery,
multiProvider: MultiProvider,
registry: IRegistry,
): Promise<ExtendedLog[]> {
const { chainId } = chainConfig;
const { chainId } = chainMetadata;
const address = query.input;
logger.debug(`Fetching logs for address ${address} on chain ${chainId}`);
const mailbox = await resolveMailbox(chainConfig, multiProvider, registry);
const mailbox = await resolveMailbox(chainMetadata, multiProvider, registry);
if (!mailbox) return [];
const dispatchTopic = addressToBytes32(address);
@ -135,7 +134,7 @@ async function fetchLogsForAddress(
}
async function fetchLogsForTxHash(
{ chainId }: ChainConfig,
{ chainId }: ChainMetadata,
query: PiMessageQuery,
multiProvider: MultiProvider,
): Promise<ExtendedLog[]> {
@ -159,15 +158,15 @@ async function fetchLogsForTxHash(
}
async function fetchLogsForMsgId(
chainConfig: ChainConfig,
chainMetadata: ChainMetadata,
query: PiMessageQuery,
multiProvider: MultiProvider,
registry: IRegistry,
): Promise<ExtendedLog[]> {
const { chainId } = chainConfig;
const { chainId } = chainMetadata;
const msgId = query.input;
logger.debug(`Fetching logs for msgId ${msgId} on chain ${chainId}`);
const mailbox = await resolveMailbox(chainConfig, multiProvider, registry);
const mailbox = await resolveMailbox(chainMetadata, multiProvider, registry);
if (!mailbox) return [];
const topic1 = msgId;
const logs: ExtendedLog[] = await fetchLogsFromProvider(
@ -186,7 +185,7 @@ async function fetchLogsForMsgId(
if (logs.length) {
const txHash = logs[0].transactionHash;
logger.debug('Found tx hash with log with msg id. Hash:', txHash);
return fetchLogsForTxHash(chainConfig, { ...query, input: txHash }, multiProvider) || [];
return fetchLogsForTxHash(chainMetadata, { ...query, input: txHash }, multiProvider) || [];
}
return [];
@ -252,7 +251,7 @@ function parseBlockTimestamp(block: providers.Block | null | undefined): number
function logToMessage(
multiProvider: MultiProvider,
log: ExtendedLog,
chainConfig: ChainConfig,
chainMetadata: ChainMetadata<{ mailbox?: Address }>,
): Message | null {
let logDesc: ethers.utils.LogDescription;
try {
@ -292,7 +291,7 @@ function logToMessage(
to: log.to ? normalizeAddress(log.to) : constants.AddressZero,
blockHash: log.blockHash,
blockNumber: BigNumber.from(log.blockNumber).toNumber(),
mailbox: chainConfig.mailbox || constants.AddressZero,
mailbox: chainMetadata.mailbox || constants.AddressZero,
nonce: 0,
// TODO get more gas info from tx
gasLimit: 0,
@ -314,10 +313,10 @@ function logToMessage(
// Fetch and sum all IGP gas payments for a given message
async function tryFetchIgpGasPayments(
message: Message,
chainConfig: ChainConfig,
chainMetadata: ChainMetadata<{ interchainGasPaymaster?: Address }>,
multiProvider: MultiProvider,
): Promise<Message> {
const { chainId, interchainGasPaymaster } = chainConfig;
const { chainId, interchainGasPaymaster } = chainMetadata;
if (!interchainGasPaymaster || !isValidAddress(interchainGasPaymaster)) {
logger.warn('No IGP address found for chain:', chainId);
return message;
@ -346,12 +345,12 @@ async function tryFetchIgpGasPayments(
}
async function resolveMailbox(
chainConfig: ChainConfig,
chainMetadata: ChainMetadata<{ mailbox?: Address }>,
multiProvider: MultiProvider,
registry: IRegistry,
) {
if (chainConfig.mailbox) return chainConfig.mailbox;
const chainName = multiProvider.getChainName(chainConfig.chainId);
if (chainMetadata.mailbox) return chainMetadata.mailbox;
const chainName = multiProvider.getChainName(chainMetadata.chainId);
const chainAddresses = await registry.getChainAddresses(chainName);
const mailbox = chainAddresses?.mailbox;
if (!mailbox) logger.debug(`No mailbox address found for chain ${chainName}`);

@ -1,13 +1,12 @@
import { useQuery } from '@tanstack/react-query';
import { IRegistry } from '@hyperlane-xyz/registry';
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { ChainMetadata, MultiProvider } from '@hyperlane-xyz/sdk';
import { ensure0x, timeout } from '@hyperlane-xyz/utils';
import { useReadyMultiProvider, useRegistry } from '../../../store';
import { Message } from '../../../types';
import { logger } from '../../../utils/logger';
import { ChainConfig } from '../../chains/chainConfig';
import { useScrapedChains } from '../../chains/queries/useScrapedChains';
import { isEvmChain, isPiChain } from '../../chains/utils';
import { isValidSearchQuery } from '../queries/useMessageQuery';
@ -16,8 +15,8 @@ import { PiMessageQuery, PiQueryType, fetchMessagesFromPiChain } from './fetchPi
const MESSAGE_SEARCH_TIMEOUT = 10_000; // 10s
// Query 'Permissionless Interoperability (PI)' chains using custom
// chain configs in store state
// Query 'Permissionless Interoperability (PI)' chains using
// override chain metadata in store state
export function usePiChainMessageSearchQuery({
sanitizedInput,
startTimeFilter,
@ -113,7 +112,7 @@ export function usePiChainMessageQuery({
}
async function fetchMessages(
chainConfig: ChainConfig,
chainMetadata: ChainMetadata,
query: PiMessageQuery,
multiProvider: MultiProvider,
registry: IRegistry,
@ -122,13 +121,13 @@ async function fetchMessages(
let messages: Message[];
try {
messages = await timeout(
fetchMessagesFromPiChain(chainConfig, query, multiProvider, registry, queryType),
fetchMessagesFromPiChain(chainMetadata, query, multiProvider, registry, queryType),
MESSAGE_SEARCH_TIMEOUT,
'message search timeout',
);
return messages;
} catch (error) {
logger.debug('Error fetching PI messages for chain:', chainConfig.name, error);
logger.debug('Error fetching PI messages for chain:', chainMetadata.name, error);
throw error;
}
}

@ -1,3 +0,0 @@
<svg width="493" height="104" viewBox="0 0 493 104" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M-2.5 11.5C-2.5 11.5 849.5 -42 311.5 112.5" stroke="white" stroke-width="5"/>
</svg>

Before

Width:  |  Height:  |  Size: 194 B

@ -1,3 +0,0 @@
<svg width="341" height="118" viewBox="0 0 341 118" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M-5 7.43544C-5 7.43544 774.5 -34.0002 -14 116.5" stroke="white" stroke-width="3"/>
</svg>

Before

Width:  |  Height:  |  Size: 199 B

@ -1,13 +0,0 @@
import type { NextPage } from 'next';
import { ConfigureChains } from '../features/chains/ConfigureChains';
const SettingsPage: NextPage = () => {
return (
<div className="mt-4 mb-2 px-2 sm:px-6 lg:pr-14 w-full">
<ConfigureChains />
</div>
);
};
export default SettingsPage;

@ -2,24 +2,25 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { GithubRegistry, IRegistry } from '@hyperlane-xyz/registry';
import { ChainMap, ChainMetadata, MultiProvider } from '@hyperlane-xyz/sdk';
import { objMap, promiseObjAll } from '@hyperlane-xyz/utils';
import { ChainMap, ChainMetadata, MultiProvider, mergeChainMetadataMap } from '@hyperlane-xyz/sdk';
import { objFilter, objMap, promiseObjAll } from '@hyperlane-xyz/utils';
import { config } from './consts/config';
import { ChainConfig } from './features/chains/chainConfig';
import { DomainsEntry } from './features/chains/queries/fragments';
import { logger } from './utils/logger';
// Increment this when persist state has breaking changes
const PERSIST_STATE_VERSION = 1;
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;
chainConfigs: ChainMap<ChainConfig>;
setChainConfigs: (configs: ChainMap<ChainConfig>) => void;
chainMetadata: ChainMap<ChainMetadata>;
setChainMetadata: (metadata: ChainMap<ChainMetadata>) => void;
chainMetadataOverrides: ChainMap<Partial<ChainMetadata>>;
setChainMetadataOverrides: (overrides?: ChainMap<Partial<ChainMetadata> | undefined>) => void;
multiProvider: MultiProvider;
setMultiProvider: (mp: MultiProvider) => void;
registry: IRegistry;
@ -33,13 +34,20 @@ export const useStore = create<AppState>()(
(set, get) => ({
scrapedChains: [],
setScrapedChains: (chains: Array<DomainsEntry>) => set({ scrapedChains: chains }),
chainConfigs: {},
setChainConfigs: async (configs: ChainMap<ChainConfig>) => {
const multiProvider = await buildMultiProvider(get().registry, configs);
set({ chainConfigs: configs, multiProvider });
chainMetadata: {},
setChainMetadata: (metadata: ChainMap<ChainMetadata>) => set({ chainMetadata: metadata }),
chainMetadataOverrides: {},
setChainMetadataOverrides: async (
overrides: ChainMap<Partial<ChainMetadata> | undefined> = {},
) => {
logger.debug('Setting chain overrides in store');
const { multiProvider } = await buildMultiProvider(get().registry, overrides);
const filtered = objFilter(overrides, (_, metadata) => !!metadata);
set({ chainMetadataOverrides: filtered, multiProvider });
},
multiProvider: new MultiProvider({}),
setMultiProvider: (multiProvider: MultiProvider) => {
logger.debug('Setting multiProvider in store');
set({ multiProvider });
},
registry: new GithubRegistry({ proxyUrl: config.githubProxy }),
@ -52,7 +60,7 @@ export const useStore = create<AppState>()(
{
name: 'hyperlane', // name in storage
version: PERSIST_STATE_VERSION,
partialize: (state) => ({ chainConfigs: state.chainConfigs }), // fields to persist
partialize: (state) => ({ chainMetadataOverrides: state.chainMetadataOverrides }), // fields to persist
onRehydrateStorage: () => {
logger.debug('Rehydrating state');
return (state, error) => {
@ -60,9 +68,10 @@ export const useStore = create<AppState>()(
logger.error('Error during hydration', error);
return;
}
buildMultiProvider(state.registry, state.chainConfigs)
.then((mp) => {
state.setMultiProvider(mp);
buildMultiProvider(state.registry, state.chainMetadataOverrides)
.then(({ metadata, multiProvider }) => {
state.setChainMetadata(metadata);
state.setMultiProvider(multiProvider);
logger.debug('Rehydration complete');
})
.catch((e) => logger.error('Error building MultiProvider', e));
@ -84,11 +93,15 @@ export function useMultiProvider() {
// otherwise returns undefined
export function useReadyMultiProvider() {
const multiProvider = useMultiProvider();
if (multiProvider.getKnownChainNames().length === 0) return undefined;
if (!multiProvider.getKnownChainNames().length) return undefined;
return multiProvider;
}
async function buildMultiProvider(registry: IRegistry, customChainConfigs: ChainMap<ChainConfig>) {
async function buildMultiProvider(
registry: IRegistry,
overrideChainMetadata: ChainMap<Partial<ChainMetadata> | undefined>,
) {
logger.debug('Building new MultiProvider from registry');
// TODO improve interface so this pre-cache isn't required
await registry.listRegistryContent();
const registryChainMetadata = await registry.getMetadata();
@ -102,5 +115,6 @@ async function buildMultiProvider(registry: IRegistry, customChainConfigs: Chain
}),
),
);
return new MultiProvider({ ...metadataWithLogos, ...customChainConfigs });
const mergedMetadata = mergeChainMetadataMap(metadataWithLogos, overrideChainMetadata);
return { metadata: metadataWithLogos, multiProvider: new MultiProvider(mergedMetadata) };
}

Loading…
Cancel
Save