feat: Update branding and replace chain picker (#114)

- Update to latest branding / styles
- Replace chain picker with new one from widgets lib
- De-dupe code with widgets lib
- Update packages to 5.4.0
- Fix #112 
- Upgrade headless and tailwind libs

Corresponds with monorepo PR:
https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/4486
pull/116/head
J M Rossy 2 months ago committed by GitHub
commit f02dd1b75f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 10
      .eslintrc
  2. 20
      package.json
  3. 1
      public/images/background.svg
  4. 12
      src/AppLayout.tsx
  5. 7
      src/components/buttons/BackButton.tsx
  6. 49
      src/components/buttons/IconButton.tsx
  7. 21
      src/components/buttons/XIconButton.tsx
  8. 134
      src/components/icons/Chevron.tsx
  9. 26
      src/components/icons/HelpIcon.tsx
  10. 38
      src/components/icons/HyperlaneLogo.tsx
  11. 31
      src/components/icons/XIcon.tsx
  12. 33
      src/components/input/Checkbox.module.css
  13. 28
      src/components/input/Checkbox.tsx
  14. 11
      src/components/input/TextField.tsx
  15. 26
      src/components/layout/BackgroundBanner.tsx
  16. 6
      src/components/layout/Card.tsx
  17. 88
      src/components/layout/Dropdown.tsx
  18. 64
      src/components/layout/Modal.tsx
  19. 132
      src/components/nav/Footer.tsx
  20. 88
      src/components/nav/Header.tsx
  21. 2
      src/components/nav/InfoBanner.tsx
  22. 17
      src/components/search/MiniSearchBar.tsx
  23. 17
      src/components/search/SearchBar.tsx
  24. 333
      src/components/search/SearchFilterBar.tsx
  25. 2
      src/components/search/SearchStates.tsx
  26. 15
      src/features/api/getMessages.ts
  27. 11
      src/features/api/getStatus.ts
  28. 15
      src/features/api/searchMessages.ts
  29. 25
      src/features/api/searchPiMessages.ts
  30. 10
      src/features/api/types.ts
  31. 8
      src/features/api/utils.ts
  32. 2
      src/features/chains/ChainConfigSyncer.tsx
  33. 40
      src/features/chains/ChainSearchModal.tsx
  34. 190
      src/features/chains/ConfigureChains.tsx
  35. 7
      src/features/chains/MissingChainConfigToast.tsx
  36. 96
      src/features/chains/chainConfig.ts
  37. 32
      src/features/chains/chainconfig.test.ts
  38. 34
      src/features/chains/queries/useScrapedChains.ts
  39. 47
      src/features/chains/useChainMetadata.ts
  40. 18
      src/features/chains/utils.ts
  41. 7
      src/features/debugger/debugMessage.ts
  42. 9
      src/features/deliveryStatus/fetchDeliveryStatus.ts
  43. 7
      src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  44. 5
      src/features/messages/cards/ContentDetailsCard.tsx
  45. 5
      src/features/messages/cards/GasDetailsCard.tsx
  46. 2
      src/features/messages/cards/IcaDetailsCard.tsx
  47. 5
      src/features/messages/cards/IsmDetailsCard.tsx
  48. 51
      src/features/messages/cards/TransactionCard.tsx
  49. 10
      src/features/messages/pi-queries/fetchPiChainMessages.test.ts
  50. 45
      src/features/messages/pi-queries/fetchPiChainMessages.ts
  51. 13
      src/features/messages/pi-queries/usePiChainMessageQuery.ts
  52. 3
      src/images/backgrounds/footer-line-desktop.svg
  53. 3
      src/images/backgrounds/footer-line-mobile.svg
  54. 2
      src/images/logos/hyperlane-explorer.svg
  55. 2
      src/images/logos/hyperlane-name.svg
  56. 0
      src/multiProvider.ts
  57. 34
      src/pages/_app.tsx
  58. 3
      src/pages/_document.tsx
  59. 13
      src/pages/settings.tsx
  60. 57
      src/store.ts
  61. 41
      src/styles/Color.ts
  62. 46
      src/styles/fonts.css
  63. 8
      src/styles/fonts.ts
  64. 17
      src/styles/global.css
  65. 3
      src/utils/browser.ts
  66. 47
      src/utils/useScrollListener.ts
  67. 5
      tailwind.config.js
  68. 364
      yarn.lock

@ -30,6 +30,14 @@
"@typescript-eslint/no-floating-promises": ["error"],
"@typescript-eslint/no-non-null-assertion": ["off"],
"@typescript-eslint/no-require-imports": ["warn"],
"jsx-a11y/alt-text": ["off"]
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
],
"jsx-a11y/alt-text": ["off"],
}
}

@ -1,18 +1,18 @@
{
"name": "@hyperlane-xyz/explorer",
"description": "An interchain explorer for the Hyperlane protocol and network.",
"version": "3.13.0",
"version": "5.4.0",
"author": "J M Rossy",
"dependencies": {
"@headlessui/react": "^1.7.17",
"@hyperlane-xyz/registry": "4.3.6",
"@hyperlane-xyz/sdk": "5.2.1",
"@hyperlane-xyz/utils": "5.2.1",
"@hyperlane-xyz/widgets": "5.2.1",
"@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6",
"@headlessui/react": "^2.1.8",
"@hyperlane-xyz/registry": "4.6.0",
"@hyperlane-xyz/sdk": "5.5.0",
"@hyperlane-xyz/utils": "5.5.0",
"@hyperlane-xyz/widgets": "5.5.0",
"@tanstack/react-query": "^5.35.5",
"bignumber.js": "^9.1.2",
"buffer": "^6.0.3",
"clsx": "^2.1.1",
"ethers": "^5.7.2",
"formik": "^2.2.9",
"graphql": "^16.6.0",
@ -23,9 +23,9 @@
"react-toastify": "^9.1.1",
"react-tooltip": "^5.26.3",
"urql": "^3.0.3",
"yaml": "^2.4.2",
"yaml": "^2.4.5",
"zod": "^3.21.2",
"zustand": "4.3.8"
"zustand": "^4.5.5"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
@ -42,7 +42,7 @@
"jest": "^29.6.3",
"postcss": "^8.4.21",
"prettier": "^2.8.4",
"tailwindcss": "^3.3.3",
"tailwindcss": "^3.4.13",
"ts-node": "^10.9.1",
"typescript": "^5.5.4"
},

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 80 KiB

@ -3,8 +3,8 @@ import { PropsWithChildren } from 'react';
import { toTitleCase } from '@hyperlane-xyz/utils';
import { Footer } from '../nav/Footer';
import { Header } from '../nav/Header';
import { Footer } from './components/nav/Footer';
import { Header } from './components/nav/Header';
interface Props {
pathName: string;
@ -25,7 +25,7 @@ export function AppLayout({ pathName, children }: PropsWithChildren<Props>) {
{/* <InfoBanner /> */}
<Header pathName={pathName} />
<div className="max-w-5xl mx-auto grow">
<main style={styles.main} className="relative min-h-full pt-3 z-20">
<main style={styles.main} className="relative min-h-full pt-3">
{children}
</main>
</div>
@ -43,10 +43,10 @@ function getHeadTitle(pathName: string) {
const styles = {
container: {
backgroundImage: 'url(/images/lines-bg-top.svg)',
backgroundSize: '94vw',
backgroundImage: 'url(/images/background.svg)',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center 80px',
backgroundPosition: 'center',
},
main: {
width: 'min(900px,96vw)',

@ -1,7 +0,0 @@
import LeftArrow from '../../images/icons/arrow-left-circle.svg';
import { IconButton, IconButtonProps } from './IconButton';
export function BackButton(props: IconButtonProps) {
return <IconButton imgSrc={LeftArrow} title="Go back" {...props} />;
}

@ -1,49 +0,0 @@
import Image from 'next/image';
import { PropsWithChildren } from 'react';
export interface IconButtonProps {
width?: number;
height?: number;
classes?: string;
onClick?: () => void;
disabled?: boolean;
imgSrc?: any;
title?: string;
type?: 'button' | 'submit';
passThruProps?: any;
}
export function IconButton(props: PropsWithChildren<IconButtonProps>) {
const {
width,
height,
classes,
onClick,
imgSrc,
disabled,
title,
type,
children,
passThruProps,
} = props;
const base = 'flex items-center justify-center transition-all';
const onHover = 'hover:opacity-70';
const onDisabled = 'disabled:opacity-50';
const onActive = 'active:opacity-60';
const allClasses = `${base} ${onHover} ${onDisabled} ${onActive} ${classes}`;
return (
<button
onClick={onClick}
type={type || 'button'}
disabled={disabled ?? false}
title={title}
className={allClasses}
{...passThruProps}
>
<Image src={imgSrc} alt={title?.substring(0, 4) || ''} width={width} height={height} />
{children}
</button>
);
}

@ -1,21 +0,0 @@
import { memo } from 'react';
import X from '../../images/icons/x.svg';
import { IconButton } from './IconButton';
function _XIconButton({
onClick,
title,
size = 20,
}: {
onClick: () => void;
title?: string;
size?: number;
}) {
return (
<IconButton imgSrc={X} title={title || 'Close'} width={size} height={size} onClick={onClick} />
);
}
export const XIconButton = memo(_XIconButton);

@ -1,134 +0,0 @@
import { memo } from 'react';
import { Color } from '../../styles/Color';
interface Props {
width?: string | number;
height?: string | number;
direction: 'n' | 'e' | 's' | 'w';
color?: string;
classes?: string;
}
function _ChevronIcon({ width, height, direction, color, classes }: Props) {
let directionClass;
switch (direction) {
case 'n':
directionClass = 'rotate-180';
break;
case 'e':
directionClass = '-rotate-90';
break;
case 's':
directionClass = '';
break;
case 'w':
directionClass = 'rotate-90';
break;
default:
throw new Error(`Invalid chevron direction ${direction}`);
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
viewBox="0 0 14 8"
className={`${directionClass} ${classes}`}
>
<path
d="M1 1l6 6 6-6"
strokeWidth="2"
stroke={color || Color.Black}
fill="none"
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export const ChevronIcon = memo(_ChevronIcon);
interface Props {
width?: string | number;
height?: string | number;
direction: 'n' | 'e' | 's' | 'w';
color?: string;
classes?: string;
}
function _HyperlaneChevron({ width, height, direction, color, classes }: Props) {
let directionClass;
switch (direction) {
case 'n':
directionClass = '-rotate-90';
break;
case 'e':
directionClass = '';
break;
case 's':
directionClass = 'rotate-90';
break;
case 'w':
directionClass = 'rotate-180';
break;
default:
throw new Error(`Invalid chevron direction ${direction}`);
}
return (
<svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="1.2 1 139.1 322"
width={width}
height={height}
className={`${directionClass} ${classes}`}
>
<path
d="M6.3 1h61.3a20 20 0 0 1 18.7 13L140 158.3a5 5 0 0 1 0 3.4l-.3.9-53.5 147.2A20 20 0 0 1 67.4 323H6.2a5 5 0 0 1-4.7-6.6l55.2-158.1L1.7 7.7A5 5 0 0 1 6.2 1Z"
fill={color || Color.Blue}
></path>
</svg>
);
}
export const HyperlaneChevron = memo(_HyperlaneChevron);
function _HyperlaneWideChevron({ width, height, direction, color, classes }: Props) {
let directionClass;
switch (direction) {
case 'n':
directionClass = '-rotate-90';
break;
case 'e':
directionClass = '';
break;
case 's':
directionClass = 'rotate-90';
break;
case 'w':
directionClass = 'rotate-180';
break;
default:
throw new Error(`Invalid chevron direction ${direction}`);
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 120.3 190"
width={width}
height={height}
className={`${directionClass} ${classes}`}
fill={color || Color.Blue}
>
<path d="M4.4 0h53c7.2 0 13.7 3 16.2 7.7l46.5 85.1a2 2 0 0 1 0 2l-.2.5-46.3 87c-2.5 4.6-9 7.7-16.3 7.7h-53c-3 0-5-2-4-4L48 92.9.4 4c-1-2 1-4 4-4Z" />
</svg>
);
}
export const HyperlaneWideChevron = memo(_HyperlaneWideChevron);

@ -1,22 +1,26 @@
import { memo } from 'react';
import Question from '../../images/icons/question-circle.svg';
import { IconButton } from '../buttons/IconButton';
import { IconButton, QuestionMarkIcon } from '@hyperlane-xyz/widgets';
function _HelpIcon({ text, size = 20 }: { text: string; size?: number }) {
import { Color } from '../../styles/Color';
function _HelpIcon({ text, size = 16 }: { text: string; size?: number }) {
const tooltipProps = {
'data-tooltip-content': text,
'data-tooltip-id': 'root-tooltip',
'data-tooltip-place': 'top-start',
};
return (
// @ts-ignore allow pass-thru tooltip props
<IconButton
imgSrc={Question}
title="Help"
width={size}
height={size}
classes="opacity-50"
passThruProps={{
'data-tooltip-content': text,
'data-tooltip-id': 'root-tooltip',
'data-tooltip-place': 'top-start',
}}
/>
className="border border-gray-400 rounded-full p-px"
{...tooltipProps}
>
<QuestionMarkIcon height={size} width={size} color={Color.lightGray} className="opacity-50" />
</IconButton>
);
}

@ -1,38 +0,0 @@
import { memo } from 'react';
function _HyperlaneLogo({
width,
height,
fill,
className = '',
}: {
width?: number | string;
height?: number | string;
fill?: string;
className?: string;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
className={className}
viewBox="0 0 117 118"
>
<path
d="M64.4787 0H88.4134C91.6788 0 94.6004 1.89614 95.7403 4.7553L116.749 57.4498C116.911 57.8563 116.913 58.3035 116.754 58.7112L116.637 59.014L116.635 59.017L95.7152 112.81C94.5921 115.698 91.6551 117.62 88.3666 117.62H64.4355C63.0897 117.62 62.1465 116.379 62.59 115.192L84.1615 57.4498L62.6428 2.45353C62.1766 1.26188 63.1208 0 64.4787 0Z"
fill={fill}
/>
<path
d="M1.99945 0H25.9342C29.1996 0 32.1211 1.89614 33.261 4.7553L54.2696 57.4498C54.4316 57.8563 54.4336 58.3035 54.275 58.7112L54.1573 59.014L54.1561 59.017L33.236 112.81C32.1129 115.698 29.1759 117.62 25.8874 117.62H1.95626C0.610483 117.62 -0.332722 116.379 0.110804 115.192L21.6823 57.4498L0.163626 2.45353C-0.302638 1.26188 0.641544 0 1.99945 0Z"
fill={fill}
/>
<path
d="M80.7202 46.2178H46.9324V71.7089H80.7202L86.2411 58.5992L80.7202 46.2178Z"
fill={fill}
/>
</svg>
);
}
export const HyperlaneLogo = memo(_HyperlaneLogo);

@ -1,31 +0,0 @@
import { memo } from 'react';
function _XIcon({
width,
height,
fill,
className = '',
}: {
width?: number | string;
height?: number | string;
fill?: string;
className?: string;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
className={className}
viewBox="20.24 19.95 56.33 56.24"
>
<path
fill={fill}
d="M27.73 76.19a7.5 7.5 0 0 1-5.3-12.8l41.34-41.34a7.5 7.5 0 0 1 10.6 10.61L33 74a7.48 7.48 0 0 1-5.27 2.19Z"
/>
<path d="M69.07 76.19a7.48 7.48 0 0 1-5.3-2.2L22.43 32.66A7.5 7.5 0 0 1 33 22.05l41.37 41.34a7.5 7.5 0 0 1-5.3 12.8Z" />
</svg>
);
}
export const XIcon = memo(_XIcon);

@ -1,33 +0,0 @@
.checkbox {
appearance: none;
background-color: #fff;
margin: 0;
width: 0.9rem;
height: 0.9rem;
border: 2px solid #1f2937;
border-radius: 0.2rem;
transform: translateY(-0.075em);
display: grid;
place-content: center;
cursor: pointer;
transition: 200ms all ease-in-out;
}
.checkbox::before {
content: '';
width: 0.9rem;
height: 0.9rem;
border-radius: 0.2rem;
transform: scale(0);
transition: 200ms all ease-in-out;
background-color: #2362c0;
}
.checkbox:checked::before {
transform: scale(1);
}
.checkbox:disabled {
border-color: #bbb;
cursor: not-allowed;
}

@ -1,28 +0,0 @@
import React from 'react';
import styles from './Checkbox.module.css';
interface Props {
checked: boolean;
onToggle: (c: boolean) => void;
name?: string;
}
export function CheckBox({ checked, onToggle, name, children }: React.PropsWithChildren<Props>) {
const onChange = () => {
onToggle(!checked);
};
return (
<label className="flex items-center cursor-pointer hover:opacity-80">
<input
type="checkbox"
name={name}
checked={checked}
onChange={onChange}
className={styles.checkbox}
/>
{children}
</label>
);
}

@ -1,11 +0,0 @@
import { Field } from 'formik';
import { ComponentProps } from 'react';
export function TextField(props: ComponentProps<typeof Field>) {
return (
<Field
className="w-100 mt-2 p-2 text-sm border border-color-gray-800 rounded focus:outline-none"
{...props}
/>
);
}

@ -1,26 +0,0 @@
import { WideChevron } from '@hyperlane-xyz/widgets';
import { useStore } from '../../store';
import { classNameToColor } from '../../styles/Color';
export function BackgroundBanner() {
const bannerClassName = useStore((s) => s.bannerClassName);
const colorClass = bannerClassName || 'bg-blue-500';
return (
<div
className={`absolute -top-5 -left-4 -right-4 h-36 rounded z-10 transition-all duration-500 ${colorClass} overflow-visible`}
>
<Chevron pos="-left-11" color={classNameToColor(colorClass)} />
<Chevron pos="-right-11" color={classNameToColor(colorClass)} />
</div>
);
}
function Chevron({ color, pos }: { color: string; pos: string }) {
return (
<div className={`absolute w-24 top-0 bottom-0 ${pos} overflow-visible`}>
<WideChevron direction="e" color={color} height="100%" width="auto" rounded />
</div>
);
}

@ -7,10 +7,6 @@ interface Props {
export function Card({ className, padding = 'p-4 sm:p-5', children }: PropsWithChildren<Props>) {
return (
<div
className={`bg-white ring ring-blue-300 rounded-3xl overflow-auto ${padding} ${className}`}
>
{children}
</div>
<div className={`bg-white rounded-xl overflow-auto ${padding} ${className}`}>{children}</div>
);
}

@ -1,88 +0,0 @@
import { Menu, Popover, Transition } from '@headlessui/react';
import { Fragment, PropsWithChildren, ReactElement, ReactNode } from 'react';
interface MenuProps {
ButtonContent: (p: { isOpen: boolean }) => ReactElement;
buttonClasses?: string;
buttonTitle?: string;
menuItems: Array<(close: () => void) => ReactElement>;
menuClasses?: string;
isFullscreen?: boolean;
}
// Uses Headless menu, which auto-closes on any item click
export function DropdownMenu({
ButtonContent,
buttonClasses,
buttonTitle,
menuItems,
menuClasses,
isFullscreen,
}: MenuProps) {
const menuItemsClass = isFullscreen
? `z-50 fixed left-0 right-0 top-20 bottom-0 w-screen bg-blue-500 focus:outline-none ${menuClasses}`
: `z-50 absolute -right-1.5 mt-3 origin-top-right rounded-md bg-white shadow-md drop-shadow-md focus:outline-none ${menuClasses}`;
return (
<Menu as="div" className="relative">
<Menu.Button title={buttonTitle} className={`flex ${buttonClasses}`}>
{({ open }) => <ButtonContent isOpen={open} />}
</Menu.Button>
<DropdownTransition>
<Menu.Items className={menuItemsClass}>
{menuItems.map((mi, i) => (
<Menu.Item key={`menu-item-${i}`}>{({ close }) => mi(close)}</Menu.Item>
))}
</Menu.Items>
</DropdownTransition>
</Menu>
);
}
interface ModalProps {
buttonContent: ReactNode;
buttonClasses?: string;
buttonTitle?: string;
modalContent: (close: () => void) => ReactElement;
modalClasses?: string;
}
// Uses Headless Popover, which is a more general purpose dropdown box
export function DropdownModal({
buttonContent,
buttonClasses,
buttonTitle,
modalContent,
modalClasses,
}: ModalProps) {
return (
<Popover className="relative">
<Popover.Button title={buttonTitle} className={`flex ${buttonClasses}`}>
{buttonContent}
</Popover.Button>
<DropdownTransition>
<Popover.Panel
className={`z-50 absolute mt-3 origin-top-right rounded-md bg-white shadow-md drop-shadow-md focus:outline-none ${modalClasses}`}
>
{({ close }) => modalContent(close)}
</Popover.Panel>
</DropdownTransition>
</Popover>
);
}
function DropdownTransition({ children }: PropsWithChildren<unknown>) {
return (
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-100"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
{children}
</Transition>
);
}

@ -1,64 +0,0 @@
import { Dialog, Transition } from '@headlessui/react';
import { Fragment, PropsWithChildren } from 'react';
import XCircle from '../../images/icons/x-circle.svg';
import { IconButton } from '../buttons/IconButton';
export function Modal({
isOpen,
title,
close,
maxWidth,
children,
}: PropsWithChildren<{ isOpen: boolean; title: string; close: () => void; maxWidth?: string }>) {
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-30" onClose={close}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel
className={`w-full ${
maxWidth || 'max-w-xs'
} max-h-[90vh] transform overflow-auto rounded-xl bg-white px-4 py-4 text-left shadow-lg transition-all`}
>
<Dialog.Title as="h3" className="font-medium text-blue-500">
{title}
</Dialog.Title>
{children}
<div className="absolute right-3 top-3">
<IconButton
imgSrc={XCircle}
onClick={close}
title="Close"
classes="hover:rotate-90"
/>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
);
}

@ -1,17 +1,12 @@
// Partly copied from https://github.com/hyperlane-xyz/hyperlane-website/blob/main/src/components/nav/Footer.tsx
import Image from 'next/image';
import Link from 'next/link';
import { HyperlaneLogo } from '@hyperlane-xyz/widgets';
import { docLinks, links } from '../../consts/links';
// import FooterLine from '../../images/backgrounds/footer-line-desktop.svg';
// import FooterLineMobile from '../../images/backgrounds/footer-line-mobile.svg';
import FooterBg from '../../images/backgrounds/footer-bg.svg';
import FooterTopBorder from '../../images/backgrounds/footer-top-border.svg';
import { Color } from '../../styles/Color';
import { Discord } from '../icons/Discord';
import { Github } from '../icons/Github';
import { HyperlaneLogo } from '../icons/HyperlaneLogo';
import { Medium } from '../icons/Medium';
import { Twitter } from '../icons/Twitter';
const footerLinks1 = [
@ -34,88 +29,69 @@ const footerLinks3 = [
{ title: 'Twitter', url: links.twitter, external: true, icon: <Twitter fill="#fff" /> },
{ title: 'Discord', url: links.discord, external: true, icon: <Discord fill="#fff" /> },
{ title: 'Github', url: links.github, external: true, icon: <Github fill="#fff" /> },
{ title: 'Blog', url: links.blog, external: true, icon: <Medium fill="#fff" /> },
];
export function Footer() {
return (
<footer className="text-white ">
<div className="relative">
<Image className="relative z-0 w-full" src={FooterBg} alt="background" />
<Image
className="absolute z-10 bottom-[1.6rem] w-full h-auto"
src={FooterTopBorder}
alt="border"
/>
</div>
<div className="px-8 py-5 bg-pink-500">
<div className="flex flex-col sm:flex-row gap-10 items-center justify-between">
<div className="flex items-center justify-center">
<div className="ml-2 w-16 sm:w-20 h-16 sm:h-20">
<HyperlaneLogo fill={Color.White} />
</div>
<div className="text-xl sm:text-2xl font-medium ml-6 space-y-1 ">
<div>Go Interchain</div>
<div>With Hyperlane</div>
</div>
{/* <div className="absolute">
<div className="hidden sm:block">
<Image src={FooterLine} alt="" />
</div>
<div className="sm:hidden">
<Image src={FooterLineMobile} alt="" />
</div>
</div> */}
<footer className="text-white px-8 pt-14 pb-5 bg-gradient-to-b from-transparent to-black/40">
<div className="flex flex-col sm:flex-row gap-10 items-center justify-between">
<div className="flex items-center justify-center">
<div className="ml-2 w-12 sm:w-14 h-12 sm:h-14">
<HyperlaneLogo color={Color.white} />
</div>
<div className="text-lg sm:text-xl font-medium ml-6 space-y-1 ">
<div>Go interchain</div>
<div>with Hyperlane</div>
</div>
<nav className="flex text-lg font-medium">
<ul className={`${styles.linkCol} mr-14`}>
{footerLinks1.map((item) => (
<li className="" key={item.title}>
<Link
className={styles.linkItem}
target={item.external ? '_blank' : '_self'}
href={item.url}
>
<div className="">{item.title}</div>
</Link>
</li>
))}
</ul>
<ul className={`${styles.linkCol} mr-14`}>
{footerLinks2.map((item) => (
<li key={item.title}>
<Link
className={styles.linkItem}
target={item.external ? '_blank' : '_self'}
href={item.url}
>
<div className="">{item.title}</div>
</Link>
</li>
))}
</ul>
<ul className={`${styles.linkCol}`}>
{footerLinks3.map((item) => (
<li key={item.title}>
<Link
className={styles.linkItem}
target={item.external ? '_blank' : '_self'}
href={item.url}
>
{item?.icon && <div className="mr-4 w-6">{item?.icon}</div>}
<div className="">{item.title}</div>
</Link>
</li>
))}
</ul>
</nav>
</div>
<nav className="flex font-medium">
<ul className={`${styles.linkCol} mr-14`}>
{footerLinks1.map((item) => (
<li className="" key={item.title}>
<Link
className={styles.linkItem}
target={item.external ? '_blank' : '_self'}
href={item.url}
>
<div className="">{item.title}</div>
</Link>
</li>
))}
</ul>
<ul className={`${styles.linkCol} mr-14`}>
{footerLinks2.map((item) => (
<li key={item.title}>
<Link
className={styles.linkItem}
target={item.external ? '_blank' : '_self'}
href={item.url}
>
<div className="">{item.title}</div>
</Link>
</li>
))}
</ul>
<ul className={`${styles.linkCol}`}>
{footerLinks3.map((item) => (
<li key={item.title}>
<Link
className={styles.linkItem}
target={item.external ? '_blank' : '_self'}
href={item.url}
>
{item?.icon && <div className="mr-4 w-5">{item?.icon}</div>}
<div className="">{item.title}</div>
</Link>
</li>
))}
</ul>
</nav>
</div>
</footer>
);
}
const styles = {
linkCol: 'flex flex-col gap-3',
linkCol: 'flex flex-col gap-2',
linkItem: 'flex items-center capitalize text-decoration-none hover:underline underline-offset-2',
};

@ -1,32 +1,22 @@
import Image from 'next/image';
import Link from 'next/link';
import { PropsWithChildren, useEffect, useState } from 'react';
import { PropsWithChildren } from 'react';
import { DropdownMenu, WideChevron } from '@hyperlane-xyz/widgets';
import { docLinks, links } from '../../consts/links';
import Explorer from '../../images/logos/hyperlane-explorer.svg';
import Logo from '../../images/logos/hyperlane-logo.svg';
import Name from '../../images/logos/hyperlane-name.svg';
import { Color } from '../../styles/Color';
import { HyperlaneWideChevron } from '../icons/Chevron';
import { DropdownMenu } from '../layout/Dropdown';
import { useScrollThresholdListener } from '../../utils/useScrollListener';
import { MiniSearchBar } from '../search/MiniSearchBar';
const PAGES_EXCLUDING_SEARCH = ['/', '/debugger'];
export function Header({ pathName }: { pathName: string }) {
// For dynamic sizing on scroll
const [animateHeader, setAnimateHeader] = useState(false);
useEffect(() => {
const listener = () => {
if (window.scrollY > 100) {
setAnimateHeader(true);
} else setAnimateHeader(false);
};
window.addEventListener('scroll', listener);
return () => {
window.removeEventListener('scroll', listener);
};
}, []);
const animateHeader = useScrollThresholdListener(100);
const showSearch = !PAGES_EXCLUDING_SEARCH.includes(pathName);
@ -35,7 +25,7 @@ export function Header({ pathName }: { pathName: string }) {
return (
<header
className={`z-30 sticky top-0 px-2 sm:px-6 lg:px-12 w-full bg-blue-500 transition-all ease-in-out duration-500 ${
className={`z-10 sticky top-0 px-2 sm:px-6 lg:px-12 w-full bg-blue-500 transition-all ease-in-out duration-200 ${
animateHeader ? 'py-1 border-b border-white' : 'py-4 sm:py-5'
}`}
>
@ -46,9 +36,9 @@ export function Header({ pathName }: { pathName: string }) {
animateHeader && 'scale-90'
} transition-all ease-in-out duration-500`}
>
<Image src={Logo} alt="" className="h-8 sm:h-10 w-auto" />
<Image src={Name} alt="Hyperlane" className="hidden sm:block h-8 w-auto mt-1 ml-3" />
<Image src={Explorer} alt="Explorer" className="h-7 sm:h-8 w-auto mt-1 ml-2.5" />
<Image src={Logo} alt="" className="h-7 sm:h-8 w-auto" />
<Image src={Name} alt="Hyperlane" className="hidden sm:block h-6 w-auto mt-1 ml-3" />
<Image src={Explorer} alt="Explorer" className="h-5 sm:h-6 w-auto mt-1 ml-2.5" />
</div>
</Link>
<nav
@ -73,46 +63,36 @@ export function Header({ pathName }: { pathName: string }) {
>
Docs
</a>
<Link href="/settings" className={navLinkClass('/settings')}>
Settings
</Link>
{showSearch && <MiniSearchBar />}
</nav>
{/* Dropdown menu, used on mobile */}
<div className="relative flex item-center sm:hidden mr-2">
<DropdownMenu
ButtonContent={DropdownButton}
buttonClasses="hover:opacity-80 active:opacity-70 transition-all"
buttonTitle="Options"
button={<DropdownButton />}
buttonClassname="hover:opacity-80 active:opacity-70 transition-all"
menuItems={[
(c: Fn) => (
<MobileNavLink href="/" closeDropdown={c} key="Home">
({ close }) => (
<MobileNavLink href="/" closeDropdown={close} key="Home">
Home
</MobileNavLink>
),
(c: Fn) => (
<MobileNavLink href="/settings" closeDropdown={c} key="Settings">
Settings
</MobileNavLink>
),
// (c: Fn) => (
// ({ close }) => (
// <MobileNavLink href="/api" closeDropdown={c} key="API">
// API
// </MobileNavLink>
// ),
(c: Fn) => (
<MobileNavLink href={docLinks.home} closeDropdown={c} key="Docs">
({ close }) => (
<MobileNavLink href={docLinks.home} closeDropdown={close} key="Docs">
Docs
</MobileNavLink>
),
(c: Fn) => (
<MobileNavLink href={links.home} closeDropdown={c} key="About">
({ close }) => (
<MobileNavLink href={links.home} closeDropdown={close} key="About">
About
</MobileNavLink>
),
]}
menuClasses="pt-8 px-8"
isFullscreen={true}
menuClassname="!left-0 !right-0 py-7 px-8 bg-blue-500"
/>
</div>
</div>
@ -120,29 +100,29 @@ export function Header({ pathName }: { pathName: string }) {
);
}
function DropdownButton({ isOpen }: { isOpen: boolean }) {
function DropdownButton() {
return (
<div className="px-4 py-1 flex flex-col items-center border border-white bg-pink-500 rounded-lg">
<HyperlaneWideChevron
<WideChevron
width={10}
height={14}
direction={isOpen ? 'n' : 's'}
color={Color.White}
classes="transition-all"
direction="s"
color={Color.white}
className="transition-all"
/>
<HyperlaneWideChevron
<WideChevron
width={10}
height={14}
direction={isOpen ? 'n' : 's'}
color={Color.White}
classes="-mt-1 transition-all"
direction="s"
color={Color.white}
className="-mt-1 transition-all"
/>
<HyperlaneWideChevron
<WideChevron
width={10}
height={14}
direction={isOpen ? 'n' : 's'}
color={Color.White}
classes="-mt-1 transition-all"
direction="s"
color={Color.white}
className="-mt-1 transition-all"
/>
</div>
);
@ -162,14 +142,14 @@ function MobileNavLink({
rel={isExternal ? 'noopener noreferrer' : undefined}
target={isExternal ? '_blank' : undefined}
>
<span className="text-2xl font-medium text-white capitalize">{children}</span>
<span className="text-xl font-medium text-white capitalize">{children}</span>
</Link>
);
}
const styles = {
navLink:
'flex items-center font-medium text-white tracking-wide hover:underline active:opacity-80 decoration-4 decoration-pink-500 underline-offset-[2px] transition-all',
'flex items-center font-medium text-white tracking-wide hover:underline active:opacity-80 decoration-4 decoration-pink-500 underline-offset-[3px] transition-all',
dropdownOption:
'flex items-center cursor-pointer p-2 mt-1 rounded text-blue-500 font-medium hover:underline decoration-2 underline-offset-4 transition-all',
};

@ -4,7 +4,7 @@ export function InfoBanner() {
href="https://explorer-v2.hyperlane.xyz"
target="_blank"
rel="noopener noreferrer"
className="block py-1.5 w-full text-white text-center text-sm bg-blue-600 hover:bg-blue-700 active:bg-blue-800 ring-1 ring-inset ring-green-700 transition-all duration-300"
className="block py-1.5 w-full text-white text-center text-sm bg-blue-600 hover:bg-blue-700 active:bg-blue-800 transition-all duration-300"
>
This is the explorer for Hyperlane version 3.{' '}
<span className="underline underline-offset-2">Use version 2</span>

@ -1,8 +1,9 @@
import { Field, Form, Formik } from 'formik';
import { useRouter } from 'next/router';
import SearchIcon from '../../images/icons/search.svg';
import { IconButton } from '../buttons/IconButton';
import { IconButton, SearchIcon } from '@hyperlane-xyz/widgets';
import { Color } from '../../styles/Color';
interface FormValues {
search: string;
@ -22,7 +23,7 @@ export function MiniSearchBar() {
return (
<Formik<FormValues> initialValues={initialValues} onSubmit={onSubmit}>
<Form>
<div className="p-1 flex items-center bg-white ring ring-blue-400 hover:ring-blue-200 rounded-full transition-all">
<div className="p-1 flex items-center bg-white rounded-full transition-all">
<Field
id="search"
name="search"
@ -31,13 +32,9 @@ export function MiniSearchBar() {
className="w-32 focus:w-64 py-2 px-2.5 h-8 text-sm font-light placeholder:text-gray-600 rounded-full focus:outline-none transition-[width] ease-in-out duration-500"
/>
<div className="h-8 w-8 flex items-center justify-center rounded-full bg-pink-500">
<IconButton
type="submit"
imgSrc={SearchIcon}
width={14}
height={14}
title="Search"
></IconButton>
<IconButton type="submit" title="Search">
<SearchIcon width={14} height={14} color={Color.white} />
</IconButton>
</div>
</div>
</Form>

@ -1,10 +1,10 @@
import Image from 'next/image';
import { ChangeEvent } from 'react';
import { IconButton, XIcon } from '@hyperlane-xyz/widgets';
import SearchIcon from '../../images/icons/search.svg';
import XIcon from '../../images/icons/x.svg';
import { Spinner } from '../animations/Spinner';
import { IconButton } from '../buttons/IconButton';
interface Props {
value: string;
@ -20,7 +20,7 @@ export function SearchBar({ value, placeholder, onChangeValue, isFetching }: Pro
};
return (
<div className="p-1 flex items-center bg-white w-full rounded-full ring ring-blue-400 hover:ring-blue-200 transition-all duration-500">
<div className="p-1 flex items-center bg-white w-full rounded-full transition-all duration-500">
<input
value={value}
onChange={onChange}
@ -36,14 +36,9 @@ export function SearchBar({ value, placeholder, onChangeValue, isFetching }: Pro
)}
{!isFetching && !value && <Image src={SearchIcon} width={20} height={20} alt="" />}
{!isFetching && value && (
<IconButton
imgSrc={XIcon}
title="Clear search"
width={16}
height={16}
onClick={() => onChange(null)}
classes="invert"
/>
<IconButton title="Clear search" onClick={() => onChange(null)}>
<XIcon width={16} height={16} color="white" />
</IconButton>
)}
</div>
</div>

@ -1,27 +1,17 @@
import Image from 'next/image';
import Link from 'next/link';
import { useMemo, useState } from 'react';
import clsx from 'clsx';
import { useState } from 'react';
import { ChainMetadata } from '@hyperlane-xyz/sdk';
import { arrayToObject } from '@hyperlane-xyz/utils';
import { trimToLength } from '@hyperlane-xyz/utils';
import { ChevronIcon, IconButton, Popover, XIcon, useModal } from '@hyperlane-xyz/widgets';
import { useScrapedChains } from '../../features/chains/queries/useScrapedChains';
import {
getChainDisplayName,
isEvmChain,
isPiChain,
isUnscrapedDbChain,
} from '../../features/chains/utils';
import GearIcon from '../../images/icons/gear.svg';
import { ChainSearchModal } from '../../features/chains/ChainSearchModal';
import { getChainDisplayName } from '../../features/chains/utils';
import { useMultiProvider } from '../../store';
import { Color } from '../../styles/Color';
import { SolidButton } from '../buttons/SolidButton';
import { TextButton } from '../buttons/TextButton';
import { ChainLogo } from '../icons/ChainLogo';
import { ChevronIcon } from '../icons/Chevron';
import { CheckBox } from '../input/Checkbox';
import { DatetimeField } from '../input/DatetimeField';
import { DropdownModal } from '../layout/Dropdown';
interface Props {
originChain: string | null;
@ -46,19 +36,11 @@ export function SearchFilterBar({
}: Props) {
return (
<div className="flex items-center space-x-2 md:space-x-4">
<ChainMultiSelector
text="Origin"
header="Origin Chains"
value={originChain}
onChangeValue={onChangeOrigin}
position="-right-32"
/>
<ChainMultiSelector
<ChainSelector text="Origin" value={originChain} onChangeValue={onChangeOrigin} />
<ChainSelector
text="Destination"
header="Destination Chains"
value={destinationChain}
onChangeValue={onChangeDestination}
position="-right-28"
/>
<DatetimeSelector
startValue={startTimestamp}
@ -66,195 +48,59 @@ export function SearchFilterBar({
endValue={endTimestamp}
onChangeEndValue={onChangeEndTimestamp}
/>
<Link href="/settings" title="View explorer settings">
<div className="p-1.5 bg-pink-500 rounded-full active:opacity-90 hover:rotate-90 transition-all">
<Image src={GearIcon} width={16} height={16} className="invert" alt="Settings" />
</div>
</Link>
</div>
);
}
function ChainMultiSelector({
function ChainSelector({
text,
header,
value,
onChangeValue,
position,
}: {
text: string;
header: string;
value: string | null; // comma separated list of checked chains
value: ChainId | null;
onChangeValue: (value: string | null) => void;
position?: string;
}) {
const { scrapedChains } = useScrapedChains();
const multiProvider = useMultiProvider();
const { chains, mainnets, testnets } = useMemo(() => {
const chains = Object.values(multiProvider.metadata);
// Filtering to EVM is necessary to prevent errors until cosmos support is added
// https://github.com/hyperlane-xyz/hyperlane-explorer/issues/61
const scrapedEvmChains = chains.filter(
(c) =>
isEvmChain(multiProvider, c.chainId) &&
!isPiChain(multiProvider, scrapedChains, c.chainId) &&
!isUnscrapedDbChain(multiProvider, c.chainId),
);
const mainnets = scrapedEvmChains.filter((c) => !c.isTestnet);
const testnets = scrapedEvmChains.filter((c) => !!c.isTestnet);
// Return only evmChains because of graphql only accept query non-evm chains (with bigint type not string)
return { chains: scrapedEvmChains, mainnets, testnets };
}, [multiProvider, scrapedChains]);
// Need local state as buffer before user hits apply
const [checkedChains, setCheckedChains] = useState(
value
? arrayToObject(value.split(','))
: arrayToObject(chains.map((c) => c.chainId.toString())),
);
const hasAnyUncheckedChain = (chains: ChainMetadata[]) => {
for (const c of chains) {
if (!checkedChains[c.chainId]) return true;
}
return false;
};
const onToggle = (chainId: string | number) => {
return (checked: boolean) => {
if (!hasAnyUncheckedChain(chains)) {
// If none are unchecked, uncheck all except this one
setCheckedChains({ [chainId]: true });
} else {
setCheckedChains({ ...checkedChains, [chainId]: checked });
}
};
};
const onToggleSection = (chains: ChainMetadata[]) => {
return () => {
const chainIds = chains.map((c) => c.chainId.toString());
if (hasAnyUncheckedChain(chains)) {
// If some are unchecked, check all
setCheckedChains({ ...checkedChains, ...arrayToObject(chainIds, true) });
} else {
// If none are unchecked, uncheck all
setCheckedChains({ ...checkedChains, ...arrayToObject(chainIds, false) });
}
};
};
const { isOpen, open, close } = useModal();
const onToggleAll = () => {
setCheckedChains(arrayToObject(chains.map((c) => c.chainId.toString())));
};
const multiProvider = useMultiProvider();
const chainName = value
? trimToLength(getChainDisplayName(multiProvider, value, true), 12)
: undefined;
const onToggleNone = () => {
setCheckedChains({});
const onClickChain = (c: ChainMetadata) => {
onChangeValue(c.chainId.toString());
close();
};
const onClickApply = (closeDropdown?: () => void) => {
const checkedList = Object.keys(checkedChains).filter((c) => !!checkedChains[c]);
if (checkedList.length === 0 || checkedList.length === chains.length) {
// Use null value, indicating to filter needed
onChangeValue(null);
} else {
onChangeValue(checkedList.join(','));
}
if (closeDropdown) closeDropdown();
const onClear = () => {
onChangeValue(null);
};
return (
<DropdownModal
buttonContent={
<>
<span className="text-white font-medium py-px">{text}</span>
<div className="relative">
<button
type="button"
className={clsx(
'text-sm sm:min-w-[5.8rem] px-1.5 sm:px-2.5 py-1 flex items-center justify-center font-medium rounded-lg border border-pink-500 hover:opacity-80 active:opacity-70 transition-all',
value ? 'bg-pink-500 text-white pr-7 sm:pr-8' : 'text-pink-500',
)}
onClick={open}
>
<span>{chainName || text} </span>
{!value && (
<ChevronIcon
direction="s"
width={9}
height={5}
classes="ml-2 opacity-80"
color={Color.White}
className="ml-2 opacity-80"
color={Color.pink}
/>
</>
}
buttonClasses="text-sm sm:min-w-[5.8rem] px-1 sm:px-2.5 py-0.5 flex items-center justify-center rounded-full bg-pink-500 hover:opacity-80 active:opacity-70 transition-all"
modalContent={(closeDropdown) => (
<div className="p-4">
<div className="flex items-center justify-between">
<h3 className="font-medium text-blue-500">{header}</h3>
<div className="flex mr-4">
<TextButton classes="text-sm font-medium text-pink-500" onClick={onToggleAll}>
All
</TextButton>
<TextButton classes="ml-3.5 text-sm font-medium text-pink-500" onClick={onToggleNone}>
None
</TextButton>
</div>
</div>
<div className="mt-2.5 flex space-x-2">
<div className="flex flex-col overflow-x-hidden overflow-y-auto max-h-100">
<div className="pb-1.5">
<CheckBox
checked={!hasAnyUncheckedChain(mainnets)}
onToggle={onToggleSection(mainnets)}
name="mainnet-chains"
>
<h4 className="ml-2 text-gray-800">Mainnet Chains</h4>
</CheckBox>
</div>
{mainnets.map((c) => (
<CheckBox
key={c.name}
checked={!!checkedChains[c.chainId]}
onToggle={onToggle(c.chainId)}
name={c.name}
>
<div className="py-0.5 ml-2 text-sm flex items-center">
<span className="mr-2 font-light">
{getChainDisplayName(multiProvider, c.chainId, true)}
</span>
<ChainLogo chainId={c.chainId} size={12} background={false} />
</div>
</CheckBox>
))}
</div>
<div className="flex flex-col overflow-x-hidden overflow-y-auto max-h-100">
<div className="pb-1.5">
<CheckBox
checked={!hasAnyUncheckedChain(testnets)}
onToggle={onToggleSection(testnets)}
name="testnet-chains"
>
<h4 className="ml-2 text-gray-800">Testnet Chains</h4>
</CheckBox>
</div>
{testnets.map((c) => (
<CheckBox
key={c.name}
checked={!!checkedChains[c.chainId]}
onToggle={onToggle(c.chainId)}
name={c.name}
>
<div className="py-0.5 ml-2 text-sm flex items-center">
<span className="mr-2 font-light">
{getChainDisplayName(multiProvider, c.chainId, true)}
</span>
<ChainLogo chainId={c.chainId} size={12} background={false} />
</div>
</CheckBox>
))}
</div>
</div>
<SolidButton
classes="mt-2.5 text-sm px-2 py-1 w-full"
onClick={() => onClickApply(closeDropdown)}
>
Apply
</SolidButton>
</div>
)}
modalClasses={`w-88 ${position || 'right-0'}`}
/>
)}
</button>
{value && <ClearButton onClick={onClear} />}
<ChainSearchModal isOpen={isOpen} close={close} onClickChain={onClickChain} />
</div>
);
}
@ -278,52 +124,79 @@ function DatetimeSelector({
setEndTime(null);
};
const onClickDirectClear = () => {
onClickClear();
onChangeStartValue(null);
onChangeEndValue(null);
};
const onClickApply = (closeDropdown?: () => void) => {
onChangeStartValue(startTime);
onChangeEndValue(endTime);
if (closeDropdown) closeDropdown();
};
const hasValue = !!startTime || !!endTime;
return (
<DropdownModal
buttonContent={
<>
<span className="text-white font-medium py-px px-2">Time</span>
<ChevronIcon
direction="s"
width={9}
height={5}
classes="ml-2 opacity-80"
color={Color.White}
/>
</>
}
buttonClasses="text-sm px-1 sm:px-2.5 py-0.5 flex items-center justify-center rounded-full bg-pink-500 hover:opacity-80 active:opacity-70 transition-all"
modalContent={(closeDropdown) => (
<div className="p-4" key="date-time-selector">
<div className="flex items-center justify-between">
<h3 className="text-blue-500 font-medium">Time Range</h3>
<div className="flex pt-1">
<TextButton classes="text-sm font-medium text-pink-500" onClick={onClickClear}>
Clear
</TextButton>
<div className="relative">
<Popover
button={
<>
<span>Time</span>
{!hasValue && (
<ChevronIcon
direction="s"
width={9}
height={5}
className="ml-2 opacity-80"
color={Color.pink}
/>
)}
</>
}
buttonClassname={clsx(
'text-sm px-2 sm:px-3 py-1 flex items-center justify-center font-medium border border-pink-500 rounded-lg hover:opacity-80 active:opacity-70 transition-all',
hasValue ? ' bg-pink-500 text-white pr-7 sm:pr-8' : 'text-pink-500',
)}
panelClassname="w-60"
>
{({ close }) => (
<div className="p-4" key="date-time-selector">
<div className="flex items-center justify-between">
<h3 className="text-blue-500 font-medium">Time Range</h3>
<div className="flex pt-1">
<TextButton classes="text-sm font-medium text-pink-500" onClick={onClickClear}>
Clear
</TextButton>
</div>
</div>
<div className="flex flex-col">
<h4 className="mt-3 mb-1 text-gray-500 text-sm font-medium">Start Time</h4>
<DatetimeField timestamp={startTime} onChange={setStartTime} />
<h4 className="mt-3 mb-1 text-gray-500 text-sm font-medium">End Time</h4>
<DatetimeField timestamp={endTime} onChange={setEndTime} />
</div>
<SolidButton
classes="mt-4 text-sm px-2 py-1 w-full"
onClick={() => onClickApply(close)}
>
Apply
</SolidButton>
</div>
<div className="flex flex-col">
<h4 className="mt-3 mb-1 text-gray-500 text-sm font-medium">Start Time</h4>
<DatetimeField timestamp={startTime} onChange={setStartTime} />
<h4 className="mt-3 mb-1 text-gray-500 text-sm font-medium">End Time</h4>
<DatetimeField timestamp={endTime} onChange={setEndTime} />
</div>
<SolidButton
classes="mt-4 text-sm px-2 py-1 w-full"
onClick={() => onClickApply(closeDropdown)}
>
Apply
</SolidButton>
</div>
)}
modalClasses="w-60 -right-8"
/>
)}
</Popover>
{hasValue && <ClearButton onClick={onClickDirectClear} />}
</div>
);
}
function ClearButton({ onClick }: { onClick: () => void }) {
return (
<div className="absolute right-1.5 top-1/2 -translate-y-1/2">
<IconButton onClick={onClick} className="bg-pink-300 p-1.5 rounded-full">
<XIcon color="white" height={9} width={9} />
</IconButton>
</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();

@ -0,0 +1,40 @@
import { ChainMetadata } from '@hyperlane-xyz/sdk';
import { ChainSearchMenu, Modal } from '@hyperlane-xyz/widgets';
import { useMultiProvider, useStore } from '../../store';
import { useScrapedEvmChains } from './queries/useScrapedChains';
export function ChainSearchModal({
isOpen,
close,
onClickChain,
showAddChainMenu,
}: {
isOpen: boolean;
close: () => void;
onClickChain?: (metadata: ChainMetadata) => void;
showAddChainMenu?: boolean;
}) {
const multiProvider = useMultiProvider();
const { chains } = useScrapedEvmChains(multiProvider);
const { chainMetadataOverrides, setChainMetadataOverrides } = useStore((s) => ({
chainMetadataOverrides: s.chainMetadataOverrides,
setChainMetadataOverrides: s.setChainMetadataOverrides,
}));
const onClick = onClickChain || (() => {});
return (
<Modal isOpen={isOpen} close={close} panelClassname="p-4 sm:p-5 max-w-lg min-h-[40vh]">
<ChainSearchMenu
chainMetadata={chains}
onClickChain={onClick}
overrideChainMetadata={chainMetadataOverrides}
onChangeOverrideMetadata={setChainMetadataOverrides}
showAddChainButton={true}
showAddChainMenu={showAddChainMenu}
/>
</Modal>
);
}

@ -1,190 +0,0 @@
import { ChangeEventHandler, useState } from 'react';
import { ChainName } from '@hyperlane-xyz/sdk';
import { CopyButton } from '../../components/buttons/CopyButton';
import { SolidButton } from '../../components/buttons/SolidButton';
import { XIconButton } from '../../components/buttons/XIconButton';
import { ChainLogo } from '../../components/icons/ChainLogo';
import { Card } from '../../components/layout/Card';
import { Modal } from '../../components/layout/Modal';
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>
<XIconButton
onClick={() => onClickRemoveChain(chain.name)}
title="Remove"
size={10}
/>
</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}
title="Add Custom Chain"
maxWidth="max-w-xl"
>
<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,5 +1,3 @@
import Link from 'next/link';
export function MissingChainConfigToast({
domainId,
chainId,
@ -14,10 +12,7 @@ export function MissingChainConfigToast({
: 'unknown message chain';
return (
<div>
<span>{`No chain config found for ${errorDesc}. `}</span>
<Link href="/settings" className="underline">
Add a config
</Link>
<span>{`No chain config found for ${errorDesc}. You can add a config in the origin/destination chain selector.`}</span>
</div>
);
}

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

@ -1,7 +1,12 @@
import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';
import { useQuery } from 'urql';
import { ChainMetadata, MultiProvider } from '@hyperlane-xyz/sdk';
import { objFilter } from '@hyperlane-xyz/utils';
import { unscrapedChainsInDb } from '../../../consts/config';
import { useStore } from '../../../store';
import { isEvmChain, isPiChain } from '../utils';
import { DOMAINS_QUERY, DomainsEntry } from './fragments';
@ -28,3 +33,30 @@ export function useScrapedChains() {
isError: !!error,
};
}
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(
chainMetadata,
(_, chainMetadata): chainMetadata is ChainMetadata =>
isEvmChain(multiProvider, chainMetadata.chainId) &&
!isPiChain(multiProvider, scrapedChains, 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, isFetching, isError };
}
// TODO: Remove once all chains in the DB are scraped
export function isUnscrapedDbChain(multiProvider: MultiProvider, chainIdOrName: number | string) {
const chainName = multiProvider.tryGetChainName(chainIdOrName);
return chainName && unscrapedChainsInDb.includes(chainName);
}

@ -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,19 +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 { unscrapedChainsInDb } from '../../consts/config';
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;
@ -24,9 +22,9 @@ export function getChainDisplayName(
chainOrDomainId?: ChainId | DomainId,
shortName = false,
fallbackToId = true,
) {
): string {
const metadata = multiProvider.tryGetChainMetadata(chainOrDomainId || 0);
if (!metadata) return fallbackToId && chainOrDomainId ? chainOrDomainId : 'Unknown';
if (!metadata) return fallbackToId && chainOrDomainId ? chainOrDomainId.toString() : 'Unknown';
const displayName = shortName ? metadata.displayNameShort : metadata.displayName;
return toTitleCase(displayName || metadata.displayName || metadata.name);
}
@ -51,9 +49,3 @@ export function isEvmChain(multiProvider: MultiProvider, chainIdOrName: number |
const protocol = multiProvider.tryGetProtocol(chainIdOrName);
return protocol === ProtocolType.Ethereum;
}
// TODO: Remove once all chains in the DB are scraped
export function isUnscrapedDbChain(multiProvider: MultiProvider, chainIdOrName: number | string) {
const chainName = multiProvider.tryGetChainName(chainIdOrName);
return chainName && unscrapedChainsInDb.includes(chainName);
}

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

@ -73,10 +73,7 @@ export function ContentDetailsCard({
<Image src={EnvelopeInfo} width={28} height={28} alt="" className="opacity-80" />
<div className="flex items-center pb-1">
<h3 className="text-blue-500 font-medium text-md mr-2">Message Details</h3>
<HelpIcon
size={16}
text="Immutable information about the message itself such as its contents."
/>
<HelpIcon text="Immutable information about the message itself such as its contents." />
</div>
</div>
<div className="flex flex-wrap gap-x-6 gap-y-4">

@ -75,10 +75,7 @@ export function GasDetailsCard({ message, blur, igpPayments = {} }: Props) {
<Image src={FuelPump} width={24} height={24} alt="" className="opacity-80" />
<div className="flex items-center pb-1">
<h3 className="text-blue-500 font-medium text-md mr-2">Interchain Gas Payments</h3>
<HelpIcon
size={16}
text="Amounts paid to the Interchain Gas Paymaster for message delivery."
/>
<HelpIcon text="Amounts paid to the Interchain Gas Paymaster for message delivery." />
</div>
</div>
<p className="text-sm font-light">

@ -31,7 +31,7 @@ export function IcaDetailsCard({ message: { originDomainId, body }, blur }: Prop
</div>
<div className="flex items-center pb-1">
<h3 className="text-blue-500 font-medium text-md mr-2">ICA Details</h3>
<HelpIcon size={16} text="Extra information for messages from/to Interchain Accounts." />
<HelpIcon text="Extra information for messages from/to Interchain Accounts." />
</div>
</div>
{decodeResult ? (

@ -22,10 +22,7 @@ export function IsmDetailsCard({ ismDetails, blur }: Props) {
<Image src={ShieldLock} width={24} height={24} alt="" className="opacity-80" />
<div className="flex items-center pb-1">
<h3 className="text-blue-500 font-medium text-md mr-2">Interchain Security Modules</h3>
<HelpIcon
size={16}
text="Details about the Interchain Security Modules (ISM) that must verify this message."
/>
<HelpIcon text="Details about the Interchain Security Modules (ISM) that must verify this message." />
</div>
</div>
<p className="text-sm font-light">

@ -1,19 +1,19 @@
import BigNumber from 'bignumber.js';
import Link from 'next/link';
import { PropsWithChildren, ReactNode, useState } from 'react';
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { isAddress, isZeroish } from '@hyperlane-xyz/utils';
import { Modal, useModal } from '@hyperlane-xyz/widgets';
import { Spinner } from '../../../components/animations/Spinner';
import { ChainLogo } from '../../../components/icons/ChainLogo';
import { HelpIcon } from '../../../components/icons/HelpIcon';
import { Card } from '../../../components/layout/Card';
import { Modal } from '../../../components/layout/Modal';
import { links } from '../../../consts/links';
import { useMultiProvider } from '../../../store';
import { MessageStatus, MessageTx } from '../../../types';
import { getDateTimeString, getHumanReadableTimeString } from '../../../utils/time';
import { ChainSearchModal } from '../../chains/ChainSearchModal';
import { getChainDisplayName, isEvmChain } from '../../chains/utils';
import { debugStatusToDesc } from '../../debugger/strings';
import { MessageDebugResult } from '../../debugger/types';
@ -70,6 +70,8 @@ export function DestinationTransactionCard({
const isDestinationEvmChain = isEvmChain(multiProvider, chainId);
const { isOpen, open, close } = useModal();
let content: ReactNode;
if (transaction) {
content = (
@ -104,22 +106,26 @@ export function DestinationTransactionCard({
);
} else if (!hasChainConfig) {
content = (
<DeliveryStatus>
<div className="flex flex-col items-center">
<div>Delivery status is unknown.</div>
<div className="mt-2 text-sm max-w-xs">
Permissionless Interoperability (PI) chains require a config.
</div>
<div className="mt-2 mb-6 text-sm max-w-xs">
Please{' '}
<Link href="/settings" className="underline underline-offset-2">
add a config
</Link>{' '}
for this chain.
<>
<DeliveryStatus>
<div className="flex flex-col items-center">
<div>Delivery status is unknown.</div>
<div className="mt-2 text-sm max-w-xs">
Permissionless Interoperability (PI) chains require a config.
</div>
<div className="mt-2 mb-6 text-sm max-w-xs">
Please{' '}
<button className="underline underline-offset-2" onClick={open}>
add metadata
</button>{' '}
for this chain.
</div>
<CallDataModal debugResult={debugResult} />
</div>
<CallDataModal debugResult={debugResult} />
</div>
</DeliveryStatus>
</DeliveryStatus>
{/* TODO get modal to auto-close after adding chain metadata */}
<ChainSearchModal isOpen={isOpen} close={close} showAddChainMenu={true} />
</>
);
} else if (status === MessageStatus.Pending) {
if (isDestinationEvmChain) {
@ -182,7 +188,7 @@ function TransactionCard({
</div>
<div className="flex items-center pb-1">
<h3 className="text-blue-500 font-medium text-md mr-2">{title}</h3>
<HelpIcon size={16} text={helpText} />
<HelpIcon text={helpText} />
</div>
</div>
{children}
@ -305,17 +311,12 @@ function CallDataModal({ debugResult }: { debugResult?: MessageDebugResult }) {
<button onClick={() => setIsOpen(true)} className={`mt-5 ${styles.textLink}`}>
View calldata details
</button>
<Modal
isOpen={isOpen}
title="Message Delivery Calldata"
close={() => setIsOpen(false)}
maxWidth="max-w-sm sm:max-w-md"
>
<Modal isOpen={isOpen} close={() => setIsOpen(false)} panelClassname="max-w-lg p-4 sm:p-5">
<div className="mt-2 flex flex-col space-y-3.5">
<p className="text-sm font-light">
{`The last step of message delivery is the recipient contract's 'handle' function. If the handle is reverting, try debugging it with `}
<a
className={`${styles.textLink} any:text-blue-500`}
className={`${styles.textLink} all:text-blue-500`}
href={links.tenderlySimDocs}
target="_blank"
rel="noopener noreferrer"

@ -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 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="2.64 13.68 182.04 42.38"><path fill="#D631B9" d="M2.64 48V13.68h25.25v5.86H9.6v7.68h16.03v5.7H9.6v9.27h18.43V48H2.64Zm27.46 0 8.98-13.06-8.02-11.56h7.44l4.37 7.05h.1l4.12-7.05h6.77L46.23 34.9 55.01 48h-7.77l-4.56-8.16h-.1L37.25 48H30.1Zm27.42 8.06V23.38h6.29v3.07h.14a8.55 8.55 0 0 1 7.25-3.75 9.5 9.5 0 0 1 7.77 3.6c1.96 2.4 2.93 5.54 2.93 9.41 0 3.97-.97 7.14-2.93 9.5a9.57 9.57 0 0 1-7.72 3.51c-3.2 0-5.57-1.17-7.1-3.5h-.1v10.84h-6.53ZM69.85 43.3c1.67 0 2.98-.66 3.94-1.97 1-1.31 1.49-3.12 1.49-5.43 0-2.46-.47-4.4-1.4-5.8-.92-1.41-2.35-2.12-4.27-2.12-1.95 0-3.39.74-4.32 2.21-.92 1.44-1.39 3.35-1.39 5.71 0 2.28.51 4.08 1.54 5.43a5.27 5.27 0 0 0 4.41 1.97ZM85.04 48V13.68h6.52V48h-6.52Zm31.75-2.98c-2.4 2.47-5.49 3.7-9.26 3.7-3.78 0-6.87-1.23-9.27-3.7a12.9 12.9 0 0 1-3.6-9.3c0-3.72 1.2-6.8 3.6-9.27 2.4-2.5 5.49-3.75 9.27-3.75 3.77 0 6.86 1.25 9.26 3.75 2.4 2.46 3.6 5.55 3.6 9.26s-1.2 6.82-3.6 9.31Zm-9.26-1.3c1.95 0 3.47-.71 4.56-2.15 1.12-1.47 1.68-3.43 1.68-5.86s-.56-4.38-1.68-5.85a5.35 5.35 0 0 0-4.56-2.21 5.4 5.4 0 0 0-4.61 2.2c-1.09 1.45-1.63 3.4-1.63 5.86 0 2.43.54 4.39 1.63 5.86a5.44 5.44 0 0 0 4.6 2.16Zm22.18-20.34v3.93h.15a9.12 9.12 0 0 1 2.54-3.17c.93-.7 2.1-1.05 3.5-1.05.68 0 1.19.06 1.54.19v5.71h-.14c-2.21-.22-3.99.26-5.33 1.44-1.34 1.19-2.02 3.04-2.02 5.57v12h-6.52V23.38h6.28Zm21.4 25.34c-3.96 0-7.08-1.22-9.35-3.65-2.27-2.46-3.41-5.6-3.41-9.4 0-3.69 1.14-6.76 3.4-9.22a11.28 11.28 0 0 1 8.74-3.75c3.88 0 6.87 1.35 8.98 4.04 2.11 2.68 3.17 6.28 3.17 10.8h-17.86a7.8 7.8 0 0 0 1.97 4.56 5.84 5.84 0 0 0 4.32 1.58c2.4 0 3.98-1 4.75-3.02h6.43a9.67 9.67 0 0 1-3.64 5.76c-1.96 1.53-4.45 2.3-7.5 2.3Zm-.52-20.98c-3.17 0-5.07 1.76-5.71 5.28h10.94a5.64 5.64 0 0 0-1.58-3.79 4.86 4.86 0 0 0-3.65-1.49Zm21.36-4.36v3.93h.14a9.12 9.12 0 0 1 2.54-3.17c.93-.7 2.1-1.05 3.5-1.05.68 0 1.2.06 1.55.19v5.71h-.15c-2.2-.22-3.98.26-5.33 1.44-1.34 1.19-2.01 3.04-2.01 5.57v12h-6.53V23.38h6.29Z"/><path fill="#fff" d="M7.64 48V13.68h25.25v5.86H14.6v7.68h16.03v5.7H14.6v9.27h18.43V48H7.64Zm27.46 0 8.98-13.06-8.02-11.56h7.44l4.37 7.05h.1l4.12-7.05h6.77L51.23 34.9 60.01 48h-7.77l-4.56-8.16h-.1L42.25 48H35.1Zm27.42 8.06V23.38h6.29v3.07h.14a8.55 8.55 0 0 1 7.25-3.75 9.5 9.5 0 0 1 7.77 3.6c1.96 2.4 2.93 5.54 2.93 9.41 0 3.97-.97 7.14-2.93 9.5a9.57 9.57 0 0 1-7.72 3.51c-3.2 0-5.57-1.17-7.1-3.5h-.1v10.84h-6.53ZM74.85 43.3c1.67 0 2.98-.66 3.94-1.97 1-1.31 1.49-3.12 1.49-5.43 0-2.46-.47-4.4-1.4-5.8-.92-1.41-2.35-2.12-4.27-2.12-1.95 0-3.39.74-4.32 2.21-.92 1.44-1.39 3.35-1.39 5.71 0 2.28.51 4.08 1.54 5.43a5.27 5.27 0 0 0 4.41 1.97ZM90.04 48V13.68h6.52V48h-6.52Zm31.75-2.98c-2.4 2.47-5.49 3.7-9.26 3.7-3.78 0-6.87-1.23-9.27-3.7a12.9 12.9 0 0 1-3.6-9.3c0-3.72 1.2-6.8 3.6-9.27 2.4-2.5 5.49-3.75 9.27-3.75 3.77 0 6.86 1.25 9.26 3.75 2.4 2.46 3.6 5.55 3.6 9.26s-1.2 6.82-3.6 9.31Zm-9.26-1.3c1.95 0 3.47-.71 4.56-2.15 1.12-1.47 1.68-3.43 1.68-5.86s-.56-4.38-1.68-5.85a5.35 5.35 0 0 0-4.56-2.21 5.4 5.4 0 0 0-4.61 2.2c-1.09 1.45-1.63 3.4-1.63 5.86 0 2.43.54 4.39 1.63 5.86a5.44 5.44 0 0 0 4.6 2.16Zm22.18-20.34v3.93h.15a9.12 9.12 0 0 1 2.54-3.17c.93-.7 2.1-1.05 3.5-1.05.68 0 1.19.06 1.54.19v5.71h-.14c-2.21-.22-3.99.26-5.33 1.44-1.34 1.19-2.02 3.04-2.02 5.57v12h-6.52V23.38h6.28Zm21.4 25.34c-3.96 0-7.08-1.22-9.35-3.65-2.27-2.46-3.41-5.6-3.41-9.4 0-3.69 1.14-6.76 3.4-9.22a11.28 11.28 0 0 1 8.74-3.75c3.88 0 6.87 1.35 8.98 4.04 2.11 2.68 3.17 6.28 3.17 10.8h-17.86a7.8 7.8 0 0 0 1.97 4.56 5.84 5.84 0 0 0 4.32 1.58c2.4 0 3.98-1 4.75-3.02h6.43a9.67 9.67 0 0 1-3.64 5.76c-1.96 1.53-4.45 2.3-7.5 2.3Zm-.52-20.98c-3.17 0-5.07 1.76-5.71 5.28h10.94a5.64 5.64 0 0 0-1.58-3.79 4.86 4.86 0 0 0-3.65-1.49Zm21.36-4.36v3.93h.14a9.12 9.12 0 0 1 2.54-3.17c.93-.7 2.1-1.05 3.5-1.05.68 0 1.2.06 1.55.19v5.71h-.15c-2.2-.22-3.98.26-5.33 1.44-1.34 1.19-2.01 3.04-2.01 5.57v12h-6.53V23.38h6.29Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="2.64 13.68 182.04 42.38"><path fill="#fff" d="M7.64 48V13.68h25.25v5.86H14.6v7.68h16.03v5.7H14.6v9.27h18.43V48H7.64Zm27.46 0 8.98-13.06-8.02-11.56h7.44l4.37 7.05h.1l4.12-7.05h6.77L51.23 34.9 60.01 48h-7.77l-4.56-8.16h-.1L42.25 48H35.1Zm27.42 8.06V23.38h6.29v3.07h.14a8.55 8.55 0 0 1 7.25-3.75 9.5 9.5 0 0 1 7.77 3.6c1.96 2.4 2.93 5.54 2.93 9.41 0 3.97-.97 7.14-2.93 9.5a9.57 9.57 0 0 1-7.72 3.51c-3.2 0-5.57-1.17-7.1-3.5h-.1v10.84h-6.53ZM74.85 43.3c1.67 0 2.98-.66 3.94-1.97 1-1.31 1.49-3.12 1.49-5.43 0-2.46-.47-4.4-1.4-5.8-.92-1.41-2.35-2.12-4.27-2.12-1.95 0-3.39.74-4.32 2.21-.92 1.44-1.39 3.35-1.39 5.71 0 2.28.51 4.08 1.54 5.43a5.27 5.27 0 0 0 4.41 1.97ZM90.04 48V13.68h6.52V48h-6.52Zm31.75-2.98c-2.4 2.47-5.49 3.7-9.26 3.7-3.78 0-6.87-1.23-9.27-3.7a12.9 12.9 0 0 1-3.6-9.3c0-3.72 1.2-6.8 3.6-9.27 2.4-2.5 5.49-3.75 9.27-3.75 3.77 0 6.86 1.25 9.26 3.75 2.4 2.46 3.6 5.55 3.6 9.26s-1.2 6.82-3.6 9.31Zm-9.26-1.3c1.95 0 3.47-.71 4.56-2.15 1.12-1.47 1.68-3.43 1.68-5.86s-.56-4.38-1.68-5.85a5.35 5.35 0 0 0-4.56-2.21 5.4 5.4 0 0 0-4.61 2.2c-1.09 1.45-1.63 3.4-1.63 5.86 0 2.43.54 4.39 1.63 5.86a5.44 5.44 0 0 0 4.6 2.16Zm22.18-20.34v3.93h.15a9.12 9.12 0 0 1 2.54-3.17c.93-.7 2.1-1.05 3.5-1.05.68 0 1.19.06 1.54.19v5.71h-.14c-2.21-.22-3.99.26-5.33 1.44-1.34 1.19-2.02 3.04-2.02 5.57v12h-6.52V23.38h6.28Zm21.4 25.34c-3.96 0-7.08-1.22-9.35-3.65-2.27-2.46-3.41-5.6-3.41-9.4 0-3.69 1.14-6.76 3.4-9.22a11.28 11.28 0 0 1 8.74-3.75c3.88 0 6.87 1.35 8.98 4.04 2.11 2.68 3.17 6.28 3.17 10.8h-17.86a7.8 7.8 0 0 0 1.97 4.56 5.84 5.84 0 0 0 4.32 1.58c2.4 0 3.98-1 4.75-3.02h6.43a9.67 9.67 0 0 1-3.64 5.76c-1.96 1.53-4.45 2.3-7.5 2.3Zm-.52-20.98c-3.17 0-5.07 1.76-5.71 5.28h10.94a5.64 5.64 0 0 0-1.58-3.79 4.86 4.86 0 0 0-3.65-1.49Zm21.36-4.36v3.93h.14a9.12 9.12 0 0 1 2.54-3.17c.93-.7 2.1-1.05 3.5-1.05.68 0 1.2.06 1.55.19v5.71h-.15c-2.2-.22-3.98.26-5.33 1.44-1.34 1.19-2.01 3.04-2.01 5.57v12h-6.53V23.38h6.29Z"/></svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

@ -7,11 +7,11 @@ import { Provider as UrqlProvider, createClient as createUrqlClient } from 'urql
import '@hyperlane-xyz/widgets/styles.css';
import { AppLayout } from '../AppLayout';
import { ErrorBoundary } from '../components/errors/ErrorBoundary';
import { AppLayout } from '../components/layout/AppLayout';
import { config } from '../consts/config';
import { ChainConfigSyncer } from '../features/chains/ChainConfigSyncer';
import '../styles/fonts.css';
import { MAIN_FONT } from '../styles/fonts';
import '../styles/global.css';
import { useIsSsr } from '../utils/ssr';
@ -35,19 +35,23 @@ export default function App({ Component, router, pageProps }: AppProps) {
return <div></div>;
}
// Note, the font definition is required both here and in _document.tsx
// Otherwise Next.js will not load the font
return (
<ErrorBoundary>
<QueryClientProvider client={reactQueryClient}>
<UrqlProvider value={urqlClient}>
<ChainConfigSyncer>
<AppLayout pathName={router.pathname}>
<Component {...pageProps} />
</AppLayout>
</ChainConfigSyncer>
</UrqlProvider>
</QueryClientProvider>
<ToastContainer transition={Zoom} position={toast.POSITION.BOTTOM_RIGHT} limit={2} />
<Tooltip id="root-tooltip" className="z-50" />
</ErrorBoundary>
<div className={`${MAIN_FONT.variable} font-sans text-black`}>
<ErrorBoundary>
<QueryClientProvider client={reactQueryClient}>
<UrqlProvider value={urqlClient}>
<ChainConfigSyncer>
<AppLayout pathName={router.pathname}>
<Component {...pageProps} />
</AppLayout>
</ChainConfigSyncer>
</UrqlProvider>
</QueryClientProvider>
<ToastContainer transition={Zoom} position={toast.POSITION.BOTTOM_RIGHT} limit={2} />
<Tooltip id="root-tooltip" className="z-50" />
</ErrorBoundary>
</div>
);
}

@ -1,6 +1,7 @@
import { Head, Html, Main, NextScript } from 'next/document';
import { links } from '../consts/links';
import { MAIN_FONT } from '../styles/fonts';
export default function Document() {
return (
@ -40,7 +41,7 @@ export default function Document() {
content="The official interchain explorer for the Hyperlane protocol and network."
/>
</Head>
<body className="text-black">
<body className={`${MAIN_FONT.variable} font-sans text-black`}>
<Main />
<NextScript />
</body>

@ -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,23 +2,25 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { GithubRegistry, IRegistry } from '@hyperlane-xyz/registry';
import { ChainMap, MultiProvider } from '@hyperlane-xyz/sdk';
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;
@ -32,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 }),
@ -51,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) => {
@ -59,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));
@ -83,13 +93,28 @@ 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();
return new MultiProvider({ ...registryChainMetadata, ...customChainConfigs });
// TODO have the registry do this automatically
const metadataWithLogos = await promiseObjAll(
objMap(
registryChainMetadata,
async (chainName, metadata): Promise<ChainMetadata> => ({
...metadata,
logoURI: (await registry.getChainLogoUri(chainName)) || undefined,
}),
),
);
const mergedMetadata = mergeChainMetadataMap(metadataWithLogos, overrideChainMetadata);
return { metadata: metadataWithLogos, multiProvider: new MultiProvider(mergedMetadata) };
}

@ -1,27 +1,16 @@
// Should match tailwind.config.js
export enum Color {
Black = '#010101',
White = '#FFFFFF',
Gray = '#6B7280',
Blue = '#2362C1',
Pink = '#D631B9',
Beige = '#F1EDE9',
Red = '#BF1B15',
}
// @ts-ignore
import { theme } from '../../tailwind.config';
// Useful for cases when using class names isn't convenient
// such as in svg fills
export function classNameToColor(className) {
switch (className) {
case 'bg-blue-500':
return Color.Blue;
case 'bg-pink-500':
return Color.Pink;
case 'bg-red-500':
return Color.Red;
case 'bg-gray-500':
return Color.Gray;
default:
throw new Error('Missing color for className: ' + className);
}
}
const themeColors = theme.extend.colors as unknown as Record<string, string>;
export const Color = {
black: themeColors.black,
white: themeColors.white,
gray: themeColors.gray[500],
lightGray: themeColors.gray[200],
primary: themeColors.blue[500],
accent: themeColors.pink[500],
blue: themeColors.blue[500],
pink: themeColors.pink[500],
red: themeColors.red[500],
} as const;

@ -1,46 +0,0 @@
@font-face {
font-family: 'Neue Haas Grotesk';
font-style: normal;
font-weight: 200;
src: url('/fonts/NeueHaasDisplayThin.woff2') format('woff2'),
url('/fonts/NeueHaasDisplayThin.woff') format('woff');
}
@font-face {
font-family: 'Neue Haas Grotesk';
font-style: normal;
font-weight: 300;
src: url('/fonts/NeueHaasDisplayLight.woff2') format('woff2'),
url('/fonts/NeueHaasDisplayLight.woff') format('woff');
}
@font-face {
font-family: 'Neue Haas Grotesk';
font-style: normal;
font-weight: 400;
src: url('/fonts/NeueHaasDisplayRoman.woff2') format('woff2'),
url('/fonts/NeueHaasDisplayRoman.woff') format('woff');
}
@font-face {
font-family: 'Neue Haas Grotesk';
font-style: normal;
font-weight: 500;
src: url('/fonts/NeueHaasDisplayMedium.woff2') format('woff2'),
url('/fonts/NeueHaasDisplayMedium.woff') format('woff');
}
@font-face {
font-family: 'Neue Haas Grotesk';
font-style: normal;
font-weight: 600;
src: url('/fonts/NeueHaasDisplayMedium.ttf');
}
@font-face {
font-family: 'Neue Haas Grotesk';
font-style: normal;
font-weight: 700;
src: url('/fonts/NeueHaasDisplayBold.woff2') format('woff2'),
url('/fonts/NeueHaasDisplayBold.woff') format('woff');
}

@ -0,0 +1,8 @@
import { Space_Grotesk } from 'next/font/google';
export const MAIN_FONT = Space_Grotesk({
subsets: ['latin'],
variable: '--font-main',
preload: true,
fallback: ['sans-serif'],
});

@ -34,6 +34,10 @@ select:focus {
outline: none;
}
:focus-visible {
outline: none;
}
/*
Background
==========
@ -68,6 +72,19 @@ body {
}
}
/* Tailwind extension to hide scrollbar */
@layer utilities {
/* Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}
/*
Input Overrides
===============

@ -0,0 +1,3 @@
export function isFirefox() {
return typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().includes('firefox');
}

@ -0,0 +1,47 @@
import { useEffect, useState } from 'react';
import { isFirefox } from './browser';
export function useScrollThresholdListener(threshold: number, debounce = 500) {
const [isAboveThreshold, setIsAbove] = useState(false);
const [isDebouncing, setIsDebouncing] = useState(false);
useEffect(() => {
let timeoutId: NodeJS.Timeout | null;
const listener = () => {
// TODO find a way to make this animation smooth in Firefox
if (isFirefox()) return;
const handleScroll = () => {
if (window.scrollY > threshold && !isAboveThreshold) {
setIsAbove(true);
setIsDebouncing(true);
} else if (window.scrollY <= threshold && isAboveThreshold) {
setIsAbove(false);
setIsDebouncing(true);
}
};
if (isDebouncing) {
if (!timeoutId) {
setTimeout(() => {
setIsDebouncing(false);
timeoutId = null;
handleScroll();
}, debounce);
}
} else {
handleScroll();
}
};
window.addEventListener('scroll', listener, { passive: true });
return () => {
window.removeEventListener('scroll', listener);
if (timeoutId) clearTimeout(timeoutId);
};
}, [threshold, debounce, isAboveThreshold, isDebouncing]);
return isAboveThreshold;
}

@ -6,12 +6,12 @@ module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
theme: {
fontFamily: {
sans: ['Neue Haas Grotesk', 'Helvetica', 'sans-serif'],
sans: ['var(--font-main)'],
serif: ['Garamond', 'serif'],
mono: ['Courier New', 'monospace'],
},
screens: {
any: '1px',
all: '1px',
xs: '480px',
...defaultTheme.screens,
},
@ -19,6 +19,7 @@ module.exports = {
colors: {
black: '#010101',
white: '#ffffff',
gray: {...defaultTheme.colors.gray, 150: '#EBEDF0', 250: '#404040', 350: '#6B6B6B'},
blue: {
50: '#E6EDF9',
100: '#CDDCF4',

@ -2025,6 +2025,25 @@ __metadata:
languageName: node
linkType: hard
"@floating-ui/core@npm:^1.6.0":
version: 1.6.8
resolution: "@floating-ui/core@npm:1.6.8"
dependencies:
"@floating-ui/utils": "npm:^0.2.8"
checksum: 87d52989c3d2cc80373bc153b7a40814db3206ce7d0b2a2bdfb63e2ff39ffb8b999b1b0ccf28e548000ebf863bf16e2bed45eab4c4d287a5dbe974ef22368d82
languageName: node
linkType: hard
"@floating-ui/dom@npm:^1.0.0":
version: 1.6.11
resolution: "@floating-ui/dom@npm:1.6.11"
dependencies:
"@floating-ui/core": "npm:^1.6.0"
"@floating-ui/utils": "npm:^0.2.8"
checksum: 8579392ad10151474869e7640af169b0d7fc2df48d4da27b6dcb1a57202329147ed986b2972787d4b8cd550c87897271b2d9c4633c2ec7d0b3ad37ce1da636f1
languageName: node
linkType: hard
"@floating-ui/dom@npm:^1.6.1":
version: 1.6.3
resolution: "@floating-ui/dom@npm:1.6.3"
@ -2035,6 +2054,32 @@ __metadata:
languageName: node
linkType: hard
"@floating-ui/react-dom@npm:^2.1.2":
version: 2.1.2
resolution: "@floating-ui/react-dom@npm:2.1.2"
dependencies:
"@floating-ui/dom": "npm:^1.0.0"
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: 2a67dc8499674e42ff32c7246bded185bb0fdd492150067caf9568569557ac4756a67787421d8604b0f241e5337de10762aee270d9aeef106d078a0ff13596c4
languageName: node
linkType: hard
"@floating-ui/react@npm:^0.26.16":
version: 0.26.24
resolution: "@floating-ui/react@npm:0.26.24"
dependencies:
"@floating-ui/react-dom": "npm:^2.1.2"
"@floating-ui/utils": "npm:^0.2.8"
tabbable: "npm:^6.0.0"
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: 903ffbee2c6726d117086e2a83f43d6ad339970758ce7979fd16cc7cf8dc0f5b869bd72c2c8ee1bcd6c63b190bb0960effd4d403e63685fb5aeed6b185041b08
languageName: node
linkType: hard
"@floating-ui/utils@npm:^0.2.0, @floating-ui/utils@npm:^0.2.1":
version: 0.2.1
resolution: "@floating-ui/utils@npm:0.2.1"
@ -2042,6 +2087,13 @@ __metadata:
languageName: node
linkType: hard
"@floating-ui/utils@npm:^0.2.8":
version: 0.2.8
resolution: "@floating-ui/utils@npm:0.2.8"
checksum: 3e3ea3b2de06badc4baebdf358b3dbd77ccd9474a257a6ef237277895943db2acbae756477ec64de65a2a1436d94aea3107129a1feeef6370675bf2b161c1abc
languageName: node
linkType: hard
"@gar/promisify@npm:^1.1.3":
version: 1.1.3
resolution: "@gar/promisify@npm:1.1.3"
@ -2058,15 +2110,18 @@ __metadata:
languageName: node
linkType: hard
"@headlessui/react@npm:^1.7.17":
version: 1.7.17
resolution: "@headlessui/react@npm:1.7.17"
"@headlessui/react@npm:^2.1.8":
version: 2.1.8
resolution: "@headlessui/react@npm:2.1.8"
dependencies:
client-only: "npm:^0.0.1"
"@floating-ui/react": "npm:^0.26.16"
"@react-aria/focus": "npm:^3.17.1"
"@react-aria/interactions": "npm:^3.21.3"
"@tanstack/react-virtual": "npm:^3.8.1"
peerDependencies:
react: ^16 || ^17 || ^18
react-dom: ^16 || ^17 || ^18
checksum: 00ad7db43bc1904a149925693f9d99d000e237d6e7206d0ded6bf43089070e0cb5b7188bef6f2f7d0d9175039dc90838f1cb9d0cc2b7479d6139da40de637fb7
react: ^18
react-dom: ^18
checksum: a82f115877dcc5e3d16a6b0502b6796a5bd3f38936835e241833a538c002d4ecfc3317868b0d1e9655e5de93201b0806f51bc10dbf32604e270cda4fc1636024
languageName: node
linkType: hard
@ -2095,13 +2150,13 @@ __metadata:
languageName: node
linkType: hard
"@hyperlane-xyz/core@npm:5.2.1":
version: 5.2.1
resolution: "@hyperlane-xyz/core@npm:5.2.1"
"@hyperlane-xyz/core@npm:5.4.1":
version: 5.4.1
resolution: "@hyperlane-xyz/core@npm:5.4.1"
dependencies:
"@arbitrum/nitro-contracts": "npm:^1.2.1"
"@eth-optimism/contracts": "npm:^0.6.0"
"@hyperlane-xyz/utils": "npm:5.2.1"
"@hyperlane-xyz/utils": "npm:5.5.0"
"@layerzerolabs/lz-evm-oapp-v2": "npm:2.0.2"
"@openzeppelin/contracts": "npm:^4.9.3"
"@openzeppelin/contracts-upgradeable": "npm:^v4.9.3"
@ -2110,7 +2165,7 @@ __metadata:
"@ethersproject/abi": "*"
"@ethersproject/providers": "*"
"@types/sinon-chai": "*"
checksum: df515d545c3a174dbadef13132a63874f0fc1e2c9cf891bf9c874ab2d4b31cc1d2cb33d660f9e99d547cc6d508f527d4dd5e1e758fc8d141de56401f4616010d
checksum: 759d3c2fd86ee6ec7bdf83a0ee5c639abc51a47c940643edae42eb0ea4a4c782621f0d6ec16d4cb19016a1c9d9c09a3b9b7606c4500816780b70cef959ca8787
languageName: node
linkType: hard
@ -2118,12 +2173,11 @@ __metadata:
version: 0.0.0-use.local
resolution: "@hyperlane-xyz/explorer@workspace:."
dependencies:
"@headlessui/react": "npm:^1.7.17"
"@hyperlane-xyz/registry": "npm:4.3.6"
"@hyperlane-xyz/sdk": "npm:5.2.1"
"@hyperlane-xyz/utils": "npm:5.2.1"
"@hyperlane-xyz/widgets": "npm:5.2.1"
"@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6"
"@headlessui/react": "npm:^2.1.8"
"@hyperlane-xyz/registry": "npm:4.6.0"
"@hyperlane-xyz/sdk": "npm:5.5.0"
"@hyperlane-xyz/utils": "npm:5.5.0"
"@hyperlane-xyz/widgets": "npm:5.5.0"
"@tanstack/react-query": "npm:^5.35.5"
"@trivago/prettier-plugin-sort-imports": "npm:^4.1.1"
"@types/jest": "npm:^29.5.3"
@ -2135,6 +2189,7 @@ __metadata:
autoprefixer: "npm:^10.4.15"
bignumber.js: "npm:^9.1.2"
buffer: "npm:^6.0.3"
clsx: "npm:^2.1.1"
eslint: "npm:^8.41.0"
eslint-config-next: "npm:^13.4.19"
eslint-config-prettier: "npm:^8.8.0"
@ -2150,46 +2205,36 @@ __metadata:
react-dom: "npm:^18.2.0"
react-toastify: "npm:^9.1.1"
react-tooltip: "npm:^5.26.3"
tailwindcss: "npm:^3.3.3"
tailwindcss: "npm:^3.4.13"
ts-node: "npm:^10.9.1"
typescript: "npm:^5.5.4"
urql: "npm:^3.0.3"
yaml: "npm:^2.4.2"
yaml: "npm:^2.4.5"
zod: "npm:^3.21.2"
zustand: "npm:4.3.8"
zustand: "npm:^4.5.5"
languageName: unknown
linkType: soft
"@hyperlane-xyz/registry@npm:4.3.2":
version: 4.3.2
resolution: "@hyperlane-xyz/registry@npm:4.3.2"
dependencies:
yaml: "npm:2.4.5"
zod: "npm:^3.21.2"
checksum: 7b1ff07074e4499f74a4c75dbbf0b7e641b3610bfc2a67785db724748d887d7b03c9dc9738b790cb56e008d6453789432c863f3b251498f77c931f196b9dab86
languageName: node
linkType: hard
"@hyperlane-xyz/registry@npm:4.3.6":
version: 4.3.6
resolution: "@hyperlane-xyz/registry@npm:4.3.6"
"@hyperlane-xyz/registry@npm:4.6.0":
version: 4.6.0
resolution: "@hyperlane-xyz/registry@npm:4.6.0"
dependencies:
yaml: "npm:2.4.5"
zod: "npm:^3.21.2"
checksum: 7cc42813f4f8b8ef09266be249f3dcec0584832166419df2f48eec3cc43ba766e58845ecc16673bf6465a711f08ff6c4fc5216da2f704bc31ef8ade52af4b6e5
checksum: 696eb36da72983645cea6beaebfd2d2e9c262b96c408c0f00d20495d9243cff9498c1ae4258089254667e490728344e6b7a44700bc02928468cfc82ec0d87000
languageName: node
linkType: hard
"@hyperlane-xyz/sdk@npm:5.2.1":
version: 5.2.1
resolution: "@hyperlane-xyz/sdk@npm:5.2.1"
"@hyperlane-xyz/sdk@npm:5.5.0":
version: 5.5.0
resolution: "@hyperlane-xyz/sdk@npm:5.5.0"
dependencies:
"@arbitrum/sdk": "npm:^4.0.0"
"@aws-sdk/client-s3": "npm:^3.74.0"
"@cosmjs/cosmwasm-stargate": "npm:^0.32.4"
"@cosmjs/stargate": "npm:^0.32.4"
"@hyperlane-xyz/core": "npm:5.2.1"
"@hyperlane-xyz/utils": "npm:5.2.1"
"@hyperlane-xyz/core": "npm:5.4.1"
"@hyperlane-xyz/utils": "npm:5.5.0"
"@safe-global/api-kit": "npm:1.3.0"
"@safe-global/protocol-kit": "npm:1.3.0"
"@safe-global/safe-deployments": "npm:1.37.8"
@ -2208,13 +2253,13 @@ __metadata:
peerDependencies:
"@ethersproject/abi": "*"
"@ethersproject/providers": "*"
checksum: 94912ab970d911d77590709f78f43139717abf2d1d1ffd4f97137d2b739a0de785517a38108e2c47e4a6090c8c7e2c63f5665aa77333ca243e5ab610b0ce2585
checksum: 0027df750bab349d82a290e4d949848e9eba2065c34342608b382cbb16b3d47556f153517c9071ed9512642fe252f7d7d5869a6908745b0842235756e027cf29
languageName: node
linkType: hard
"@hyperlane-xyz/utils@npm:5.2.1":
version: 5.2.1
resolution: "@hyperlane-xyz/utils@npm:5.2.1"
"@hyperlane-xyz/utils@npm:5.5.0":
version: 5.5.0
resolution: "@hyperlane-xyz/utils@npm:5.5.0"
dependencies:
"@cosmjs/encoding": "npm:^0.32.4"
"@solana/web3.js": "npm:^1.78.0"
@ -2223,20 +2268,23 @@ __metadata:
lodash-es: "npm:^4.17.21"
pino: "npm:^8.19.0"
yaml: "npm:2.4.5"
checksum: 67c725cbb0581f6a8b42723974986b9c7beee69a51dcc3261efc9c4961f27493eb10108407b1d54329eb95c19c5595ca6cc581137f925773ec600eb72c761fe1
checksum: 24c32b3badd15812dee980e8873287b6cb9eadaa2c7d99763d5d3a4a297a6717d577179d57556996963cc64dccdefae0e0eeea8e9d3f2e7b4cf83a2092161e70
languageName: node
linkType: hard
"@hyperlane-xyz/widgets@npm:5.2.1":
version: 5.2.1
resolution: "@hyperlane-xyz/widgets@npm:5.2.1"
"@hyperlane-xyz/widgets@npm:5.5.0":
version: 5.5.0
resolution: "@hyperlane-xyz/widgets@npm:5.5.0"
dependencies:
"@hyperlane-xyz/registry": "npm:4.3.2"
"@hyperlane-xyz/sdk": "npm:5.2.1"
"@headlessui/react": "npm:^2.1.8"
"@hyperlane-xyz/sdk": "npm:5.5.0"
"@hyperlane-xyz/utils": "npm:5.5.0"
clsx: "npm:^2.1.1"
react-tooltip: "npm:^5.28.0"
peerDependencies:
react: ^18
react-dom: ^18
checksum: 9ee820be26d51cf59285ca370c88867c02b828473a6ffb136590d9d7987a109b9b956620833945ca32df2345fda3d5b1a4352d2a3e94a519b633fb2fd6f1342f
checksum: f760fd5698c55ebeb2403788d950e006baf261e10579cd5c4b71d26688984ebe265cc7c930a5a881c7dfc4e4ec79bd3f4ae23eb59b773e3f3369bc987ad56d2d
languageName: node
linkType: hard
@ -2689,15 +2737,6 @@ __metadata:
languageName: node
linkType: hard
"@metamask/jazzicon@https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6":
version: 2.1.0
resolution: "@metamask/jazzicon@https://github.com/jmrossy/jazzicon.git#commit=7a8df28974b4e81129bfbe3cab76308b889032a6"
dependencies:
mersenne-twister: "npm:^1.1.0"
checksum: 5e56251b375eade58294334783fb37a15e8fd48d792f6dc93f7247b8897541324f9cf2d3f1d9b1cffdac1d932a8bc48a89dee7cdbd6e4a312ca2ff85df90131b
languageName: node
linkType: hard
"@next/env@npm:13.5.6":
version: 13.5.6
resolution: "@next/env@npm:13.5.6"
@ -3009,6 +3048,81 @@ __metadata:
languageName: node
linkType: hard
"@react-aria/focus@npm:^3.17.1":
version: 3.18.3
resolution: "@react-aria/focus@npm:3.18.3"
dependencies:
"@react-aria/interactions": "npm:^3.22.3"
"@react-aria/utils": "npm:^3.25.3"
"@react-types/shared": "npm:^3.25.0"
"@swc/helpers": "npm:^0.5.0"
clsx: "npm:^2.0.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
checksum: b11632e638de2f40ec12a4a8c818059b9bf7e90b288a93b46985350c887ae7ecdf037391537f86fbacb2a186dec7e7c41a8f2ff767fd232a8cac3189f03735b2
languageName: node
linkType: hard
"@react-aria/interactions@npm:^3.21.3, @react-aria/interactions@npm:^3.22.3":
version: 3.22.3
resolution: "@react-aria/interactions@npm:3.22.3"
dependencies:
"@react-aria/ssr": "npm:^3.9.6"
"@react-aria/utils": "npm:^3.25.3"
"@react-types/shared": "npm:^3.25.0"
"@swc/helpers": "npm:^0.5.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
checksum: bc1e8381bda81c106d64bb6eebe06c244bcd6905d1be95fdc26bad1c5d83c48d1ec5159fb1cb8ea9ee7ebafc76595702e2d174f3c8394b766779c0d34bfa6de7
languageName: node
linkType: hard
"@react-aria/ssr@npm:^3.9.6":
version: 3.9.6
resolution: "@react-aria/ssr@npm:3.9.6"
dependencies:
"@swc/helpers": "npm:^0.5.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
checksum: ea6b290346ce1e119ed9233fc0e34693d52ab9dc2509f07ab10710409b89484a544b7f26c1438802e97f3fb634844ae54638850cdd95caca0d1f5571781bf982
languageName: node
linkType: hard
"@react-aria/utils@npm:^3.25.3":
version: 3.25.3
resolution: "@react-aria/utils@npm:3.25.3"
dependencies:
"@react-aria/ssr": "npm:^3.9.6"
"@react-stately/utils": "npm:^3.10.4"
"@react-types/shared": "npm:^3.25.0"
"@swc/helpers": "npm:^0.5.0"
clsx: "npm:^2.0.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
checksum: 86aed35da5cb0d48d949e40bf8226d5a6d6c92a8cdc60e3e12d524d1f3cc91ab6b54c5e1642823773cbb889fb61af7da22e89488b704b56fc5f4d8d59da7519b
languageName: node
linkType: hard
"@react-stately/utils@npm:^3.10.4":
version: 3.10.4
resolution: "@react-stately/utils@npm:3.10.4"
dependencies:
"@swc/helpers": "npm:^0.5.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
checksum: 8a56b4d0cf8d5a7a692d6f94ffff63feac2d7078fbc5642b94b0afcaaf7c8f7f4682cfe546f98265034c52576c198be5502cff3f9b145137884e50eb9ffb96d5
languageName: node
linkType: hard
"@react-types/shared@npm:^3.25.0":
version: 3.25.0
resolution: "@react-types/shared@npm:3.25.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
checksum: fa31eb6153c223210c2eee46934a63b922917bcde0ee583f2cfe59675db122c10e1cbae6549b1fea4284391fdbeca6888b36e9dc797231ad4a76def01490aea5
languageName: node
linkType: hard
"@rushstack/eslint-patch@npm:^1.1.3":
version: 1.1.4
resolution: "@rushstack/eslint-patch@npm:1.1.4"
@ -3824,6 +3938,15 @@ __metadata:
languageName: node
linkType: hard
"@swc/helpers@npm:^0.5.0":
version: 0.5.13
resolution: "@swc/helpers@npm:0.5.13"
dependencies:
tslib: "npm:^2.4.0"
checksum: 6ba2f7e215d32d71fce139e2cfc426b3ed7eaa709febdeb07b97260a4c9eea4784cf047cc1271be273990b08220b576b94a42b5780947c0b3be84973a847a24d
languageName: node
linkType: hard
"@szmarczak/http-timer@npm:^4.0.5":
version: 4.0.6
resolution: "@szmarczak/http-timer@npm:4.0.6"
@ -3860,6 +3983,25 @@ __metadata:
languageName: node
linkType: hard
"@tanstack/react-virtual@npm:^3.8.1":
version: 3.10.8
resolution: "@tanstack/react-virtual@npm:3.10.8"
dependencies:
"@tanstack/virtual-core": "npm:3.10.8"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 40a5d6089908096634fec2aa0cd646ca47c044c745e1b0d190ecbf9905ad2e6266ccd56c2550ed92f47349954dc11eb6930beac1354441ce7c98af81c5454d3f
languageName: node
linkType: hard
"@tanstack/virtual-core@npm:3.10.8":
version: 3.10.8
resolution: "@tanstack/virtual-core@npm:3.10.8"
checksum: 047e95fa72a0d341c0da8468799c176fd448481432f976a4780911bb4a2256aa4788d828f79fad78d127fe859b785189c13ca0fea10c560bf14d8ab8cb2c7790
languageName: node
linkType: hard
"@tootallnate/once@npm:2":
version: 2.0.0
resolution: "@tootallnate/once@npm:2.0.0"
@ -5497,7 +5639,7 @@ __metadata:
languageName: node
linkType: hard
"client-only@npm:0.0.1, client-only@npm:^0.0.1":
"client-only@npm:0.0.1":
version: 0.0.1
resolution: "client-only@npm:0.0.1"
checksum: 0c16bf660dadb90610553c1d8946a7fdfb81d624adea073b8440b7d795d5b5b08beb3c950c6a2cf16279365a3265158a236876d92bce16423c485c322d7dfaf8
@ -5531,6 +5673,13 @@ __metadata:
languageName: node
linkType: hard
"clsx@npm:^2.0.0, clsx@npm:^2.1.1":
version: 2.1.1
resolution: "clsx@npm:2.1.1"
checksum: cdfb57fa6c7649bbff98d9028c2f0de2f91c86f551179541cf784b1cfdc1562dcb951955f46d54d930a3879931a980e32a46b598acaea274728dbe068deca919
languageName: node
linkType: hard
"co@npm:^4.6.0":
version: 4.6.0
resolution: "co@npm:4.6.0"
@ -6999,7 +7148,7 @@ __metadata:
languageName: node
linkType: hard
"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12":
"fast-glob@npm:^3.2.11":
version: 3.2.12
resolution: "fast-glob@npm:3.2.12"
dependencies:
@ -7025,6 +7174,19 @@ __metadata:
languageName: node
linkType: hard
"fast-glob@npm:^3.3.0":
version: 3.3.2
resolution: "fast-glob@npm:3.3.2"
dependencies:
"@nodelib/fs.stat": "npm:^2.0.2"
"@nodelib/fs.walk": "npm:^1.2.3"
glob-parent: "npm:^5.1.2"
merge2: "npm:^1.3.0"
micromatch: "npm:^4.0.4"
checksum: 222512e9315a0efca1276af9adb2127f02105d7288fa746145bf45e2716383fb79eb983c89601a72a399a56b7c18d38ce70457c5466218c5f13fad957cee16df
languageName: node
linkType: hard
"fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0":
version: 2.1.0
resolution: "fast-json-stable-stringify@npm:2.1.0"
@ -8962,12 +9124,12 @@ __metadata:
languageName: node
linkType: hard
"jiti@npm:^1.18.2":
version: 1.20.0
resolution: "jiti@npm:1.20.0"
"jiti@npm:^1.21.0":
version: 1.21.6
resolution: "jiti@npm:1.21.6"
bin:
jiti: bin/jiti.js
checksum: c4e59419dcf5599e599602c6c6bd0b3e19748c0bce886887cc91542ea085ef11f69a25dbda2b0ac7af8085afda34eef89ac6e9311949a01839c52a9af4352ec2
checksum: 289b124cea411c130a14ffe88e3d38376ab44b6695616dfa0a1f32176a8f20ec90cdd6d2b9d81450fc6467cfa4d865f04f49b98452bff0f812bc400fd0ae78d6
languageName: node
linkType: hard
@ -9457,13 +9619,6 @@ __metadata:
languageName: node
linkType: hard
"mersenne-twister@npm:^1.1.0":
version: 1.1.0
resolution: "mersenne-twister@npm:1.1.0"
checksum: 1123526199091097102f2f91639ad7d5b3df4b098de9a4a72c835920e11ef0ce08e25737d5af1d363325a60da8804365eae8a41e03b7a46a1acc22e18fa8f261
languageName: node
linkType: hard
"methods@npm:~1.1.2":
version: 1.1.2
resolution: "methods@npm:1.1.2"
@ -11000,6 +11155,19 @@ __metadata:
languageName: node
linkType: hard
"react-tooltip@npm:^5.28.0":
version: 5.28.0
resolution: "react-tooltip@npm:5.28.0"
dependencies:
"@floating-ui/dom": "npm:^1.6.1"
classnames: "npm:^2.3.0"
peerDependencies:
react: ">=16.14.0"
react-dom: ">=16.14.0"
checksum: ec13ad0fafcae51c9c1193c6f0bccba4e7047e9d02eaf77231474cefd1a3d05254e76f27229808e79dad4c0a8c47b8e5cafdad47920e34a11d7a2703adf5f998
languageName: node
linkType: hard
"react@npm:^18.2.0":
version: 18.2.0
resolution: "react@npm:18.2.0"
@ -12004,19 +12172,26 @@ __metadata:
languageName: node
linkType: hard
"tailwindcss@npm:^3.3.3":
version: 3.3.3
resolution: "tailwindcss@npm:3.3.3"
"tabbable@npm:^6.0.0":
version: 6.2.0
resolution: "tabbable@npm:6.2.0"
checksum: 980fa73476026e99dcacfc0d6e000d41d42c8e670faf4682496d30c625495e412c4369694f2a15cf1e5252d22de3c396f2b62edbe8d60b5dadc40d09e3f2dde3
languageName: node
linkType: hard
"tailwindcss@npm:^3.4.13":
version: 3.4.13
resolution: "tailwindcss@npm:3.4.13"
dependencies:
"@alloc/quick-lru": "npm:^5.2.0"
arg: "npm:^5.0.2"
chokidar: "npm:^3.5.3"
didyoumean: "npm:^1.2.2"
dlv: "npm:^1.1.3"
fast-glob: "npm:^3.2.12"
fast-glob: "npm:^3.3.0"
glob-parent: "npm:^6.0.2"
is-glob: "npm:^4.0.3"
jiti: "npm:^1.18.2"
jiti: "npm:^1.21.0"
lilconfig: "npm:^2.1.0"
micromatch: "npm:^4.0.5"
normalize-path: "npm:^3.0.0"
@ -12033,7 +12208,7 @@ __metadata:
bin:
tailwind: lib/cli.js
tailwindcss: lib/cli.js
checksum: bc47f40cc33aca95fb9d523ecef0a450241e51d2259c354ac283c6a06c4dcd7edd1ffbd6f065fc496390ff3ab4dd8349c968b10cce7e11e0bde101705fa0f4f1
checksum: 01b8dd35a65a028474c632b9ea7fb38634060a2c70f1f3fdfa2fe6ec74dec8224e2ee1178a5428182849790dad324e7a810de7301a9126946528c59d37f455cf
languageName: node
linkType: hard
@ -12513,12 +12688,12 @@ __metadata:
languageName: node
linkType: hard
"use-sync-external-store@npm:1.2.0":
version: 1.2.0
resolution: "use-sync-external-store@npm:1.2.0"
"use-sync-external-store@npm:1.2.2":
version: 1.2.2
resolution: "use-sync-external-store@npm:1.2.2"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: a676216affc203876bd47981103f201f28c2731361bb186367e12d287a7566763213a8816910c6eb88265eccd4c230426eb783d64c373c4a180905be8820ed8e
checksum: 671e9c190aab9a8374a5d468c6ba17f52c38b6fae970110bc196fc1e2b57204149aea9619be49a1bb5207fb6e51d8afd19c3bcb94afe61813fed039821461dc0
languageName: node
linkType: hard
@ -13247,12 +13422,12 @@ __metadata:
languageName: node
linkType: hard
"yaml@npm:^2.4.2":
version: 2.4.2
resolution: "yaml@npm:2.4.2"
"yaml@npm:^2.4.5":
version: 2.5.1
resolution: "yaml@npm:2.5.1"
bin:
yaml: bin.mjs
checksum: 6eafbcd68dead734035f6f72af21bd820c29214caf7d8e40c595671a3c908535cef8092b9660a1c055c5833aa148aa640e0c5fa4adb5af2dacd6d28296ccd81c
checksum: 0eecb679db75ea6a989ad97715a9fa5d946972945aa6aa7d2175bca66c213b5564502ccb1cdd04b1bf816ee38b5c43e4e2fda3ff6f5e09da24dabb51ae92c57d
languageName: node
linkType: hard
@ -13308,19 +13483,22 @@ __metadata:
languageName: node
linkType: hard
"zustand@npm:4.3.8":
version: 4.3.8
resolution: "zustand@npm:4.3.8"
"zustand@npm:^4.5.5":
version: 4.5.5
resolution: "zustand@npm:4.5.5"
dependencies:
use-sync-external-store: "npm:1.2.0"
use-sync-external-store: "npm:1.2.2"
peerDependencies:
immer: ">=9.0"
"@types/react": ">=16.8"
immer: ">=9.0.6"
react: ">=16.8"
peerDependenciesMeta:
"@types/react":
optional: true
immer:
optional: true
react:
optional: true
checksum: 95a5335716414c8bef3a48165226ef099ca232931ab6cd1497515ee4241e8d5a8100edf5c3cc7d7131b72a07eb0484501405aa2c3222b4b93ba690cfa2b5593d
checksum: 481b8210187b69678074a1ca51107654c2379688e90407bfcb7961e0803a259742bfd0d77171c3f07e290896ad55fe9659b3863f30d34cb2572650ead1249f25
languageName: node
linkType: hard

Loading…
Cancel
Save