From 8e3cff4a28ee180c9ed685f9d39290ac6acd4509 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Thu, 22 Sep 2022 16:55:56 -0400 Subject: [PATCH 01/11] Break apart message search into reusuable components --- src/components/nav/Header.tsx | 21 ++++- src/components/search/SearchBar.tsx | 47 ++++++++++ src/components/search/SearchError.tsx | 76 ++++++++++++++++ src/features/debugger/TxDebugger.tsx | 3 + src/features/search/MessageSearch.tsx | 124 +++++--------------------- src/features/search/utils.ts | 4 +- src/images/icons/bug.svg | 4 + src/pages/debugger.tsx | 14 +++ 8 files changed, 187 insertions(+), 106 deletions(-) create mode 100644 src/components/search/SearchBar.tsx create mode 100644 src/components/search/SearchError.tsx create mode 100644 src/features/debugger/TxDebugger.tsx create mode 100644 src/images/icons/bug.svg create mode 100755 src/pages/debugger.tsx diff --git a/src/components/nav/Header.tsx b/src/components/nav/Header.tsx index ed1dd55..2ef2b8f 100644 --- a/src/components/nav/Header.tsx +++ b/src/components/nav/Header.tsx @@ -5,6 +5,7 @@ import useDropdownMenu from 'react-accessible-dropdown-menu-hook'; import { Environment, allConfigs, config } from '../../consts/appConfig'; import { links } from '../../consts/links'; import BookIcon from '../../images/icons/book.svg'; +import BugIcon from '../../images/icons/bug.svg'; import HamburgerIcon from '../../images/icons/hamburger.svg'; import HouseIcon from '../../images/icons/house.svg'; import HubIcon from '../../images/icons/hub.svg'; @@ -13,7 +14,7 @@ import Logo from '../../images/logos/hyperlane-logo.svg'; import Name from '../../images/logos/hyperlane-name.svg'; export function Header() { - const { buttonProps, itemProps, isOpen, setIsOpen } = useDropdownMenu(4); + const { buttonProps, itemProps, isOpen, setIsOpen } = useDropdownMenu(5); const closeDropdown = () => { setIsOpen(false); }; @@ -41,6 +42,9 @@ export function Header() { Home + + Transaction Debugger + + + + + + void; + fetching: boolean; +} + +export function SearchBar({ value, onChangeValue, fetching }: Props) { + const onChange = (event: ChangeEvent | null) => { + const value = event?.target?.value || ''; + onChangeValue(value); + }; + + return ( +
+ +
+ {fetching && } + {!fetching && !value && ( + + )} + {!fetching && value && ( + onChange(null)} + /> + )} +
+
+ ); +} diff --git a/src/components/search/SearchError.tsx b/src/components/search/SearchError.tsx new file mode 100644 index 0000000..52e87d7 --- /dev/null +++ b/src/components/search/SearchError.tsx @@ -0,0 +1,76 @@ +import Image from 'next/future/image'; + +import ErrorIcon from '../../images/icons/error-circle.svg'; +import SearchOffIcon from '../../images/icons/search-off.svg'; +import ShrugIcon from '../../images/icons/shrug.svg'; +import { Fade } from '../animation/Fade'; + +export function SearchError({ + show, + text, + imgSrc, + imgWidth, +}: { + show: boolean; + text: string; + imgSrc: any; + imgWidth: number; +}) { + return ( + // Absolute position for overlaying cross-fade +
+ +
+
+ +
+ {text} +
+
+
+
+
+ ); +} + +export function SearchInvalidError({ show }: { show: boolean }) { + return ( + + ); +} + +export function SearchEmptyError({ + show, + hasInput, +}: { + show: boolean; + hasInput: boolean; +}) { + return ( + + ); +} + +export function SearchUnknownError({ show }: { show: boolean }) { + return ( + + ); +} diff --git a/src/features/debugger/TxDebugger.tsx b/src/features/debugger/TxDebugger.tsx new file mode 100644 index 0000000..aa17c16 --- /dev/null +++ b/src/features/debugger/TxDebugger.tsx @@ -0,0 +1,3 @@ +export function TxDebugger() { + return
TODO
; +} diff --git a/src/features/search/MessageSearch.tsx b/src/features/search/MessageSearch.tsx index 3c97343..2af558b 100644 --- a/src/features/search/MessageSearch.tsx +++ b/src/features/search/MessageSearch.tsx @@ -1,21 +1,20 @@ import Image from 'next/future/image'; -import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { toast } from 'react-toastify'; import { useQuery } from 'urql'; import { Fade } from '../../components/animation/Fade'; -import { Spinner } from '../../components/animation/Spinner'; -import { IconButton } from '../../components/buttons/IconButton'; import { SelectField } from '../../components/input/SelectField'; +import { SearchBar } from '../../components/search/SearchBar'; +import { + SearchEmptyError, + SearchInvalidError, + SearchUnknownError, +} from '../../components/search/SearchError'; import { chainToDomain } from '../../consts/domains'; import { prodChains } from '../../consts/networksConfig'; import ArrowRightIcon from '../../images/icons/arrow-right-short.svg'; -import ErrorIcon from '../../images/icons/error-circle.svg'; import FunnelIcon from '../../images/icons/funnel.svg'; -import SearchOffIcon from '../../images/icons/search-off.svg'; -import SearchIcon from '../../images/icons/search.svg'; -import ShrugIcon from '../../images/icons/shrug.svg'; -import XIcon from '../../images/icons/x.svg'; import { trimLeading0x } from '../../utils/addresses'; import useDebounce from '../../utils/debounce'; import { sanitizeString, trimToLength } from '../../utils/string'; @@ -45,14 +44,12 @@ export function MessageSearch() { // Search text input const [searchInput, setSearchInput] = useState(''); - const onChangeSearch = (event: ChangeEvent | null) => { - const value = event?.target?.value || ''; - setSearchInput(value); - }; const debouncedSearchInput = useDebounce(searchInput, 750); const hasInput = !!debouncedSearchInput; const sanitizedInput = sanitizeString(debouncedSearchInput); - const isValidInput = hasInput ? isValidSearchQuery(sanitizedInput) : true; + const isValidInput = hasInput + ? isValidSearchQuery(sanitizedInput, true) + : true; // Filter state and handlers const chainOptions = useMemo(getChainOptionList, []); @@ -88,35 +85,15 @@ export function MessageSearch() { return ( <> - {/* Search bar */} -
- -
- {fetching && } - {!fetching && !searchInput && ( - - )} - {!fetching && searchInput && ( - onChangeSearch(null)} - /> - )} -
-
+
{/* Content header and filter bar */}
-

+

{!hasInput ? 'Latest Messages' : 'Search Results'}

@@ -160,75 +137,22 @@ export function MessageSearch() {
))} - {/* Invalid input state */} - - {/* No results state */} - + + + + - {/* Search error state */} -
); } -function SearchInfoBox({ - show, - text, - imgSrc, - imgAlt, - imgWidth, -}: { - show: boolean; - text: string; - imgSrc: any; - imgAlt: string; - imgWidth: number; -}) { - return ( - // Absolute position for overlaying cross-fade -
- -
-
- {imgAlt} -
- {text} -
-
-
-
-
- ); -} - function getChainOptionList(): Array<{ value: string; display: string }> { return [ { value: '', display: 'All Chains' }, diff --git a/src/features/search/utils.ts b/src/features/search/utils.ts index b2a1245..44ead73 100644 --- a/src/features/search/utils.ts +++ b/src/features/search/utils.ts @@ -4,10 +4,10 @@ import { isValidTransactionHash, } from '../../utils/addresses'; -export function isValidSearchQuery(input: string) { +export function isValidSearchQuery(input: string, allowAddress?: boolean) { if (!input) return false; if (isValidTransactionHash(input)) return true; - if (isValidAddressFast(input)) return true; + if (allowAddress && isValidAddressFast(input)) return true; return false; } diff --git a/src/images/icons/bug.svg b/src/images/icons/bug.svg new file mode 100644 index 0000000..3be5a00 --- /dev/null +++ b/src/images/icons/bug.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/pages/debugger.tsx b/src/pages/debugger.tsx new file mode 100755 index 0000000..ab90d22 --- /dev/null +++ b/src/pages/debugger.tsx @@ -0,0 +1,14 @@ +import type { NextPage } from 'next'; + +import { ContentFrame } from '../components/layout/ContentFrame'; +import { TxDebugger } from '../features/debugger/TxDebugger'; + +const Debugger: NextPage = () => { + return ( + + + + ); +}; + +export default Debugger; From e27b9e4028e13a6d679ec14e7358f786b3944f2c Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Fri, 23 Sep 2022 18:42:52 -0400 Subject: [PATCH 02/11] Increase prettier print width --- .prettierrc | 1 + src/components/animation/Spinner.tsx | 4 +- .../buttons/ConnectAwareSubmitButton.tsx | 12 +--- src/components/buttons/IconButton.tsx | 11 +--- src/components/buttons/SolidButton.tsx | 3 +- src/components/icons/ChainIcon.tsx | 14 +---- src/components/layout/ContentFrame.tsx | 11 +--- src/components/nav/Footer.tsx | 13 +--- src/components/nav/Header.tsx | 62 ++++--------------- src/components/search/SearchBar.tsx | 4 +- src/components/search/SearchError.tsx | 12 +--- src/consts/networksConfig.ts | 11 ++-- src/features/search/MessageDetails.tsx | 51 ++++----------- src/features/search/MessageSearch.tsx | 38 +++--------- src/features/search/MessageSummary.tsx | 31 ++-------- src/features/search/placeholderMessages.ts | 9 +-- src/features/search/query.ts | 19 ++---- src/features/search/utils.ts | 5 +- src/pages/_app.tsx | 17 +---- src/pages/_document.tsx | 30 ++------- src/pages/message/[messageId].tsx | 4 +- src/styles/globals.css | 3 +- src/utils/addresses.ts | 4 +- src/utils/objects.ts | 4 +- src/utils/string.ts | 4 +- src/utils/timeout.ts | 9 +-- 26 files changed, 77 insertions(+), 309 deletions(-) diff --git a/.prettierrc b/.prettierrc index f6b3475..8065834 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,6 @@ { "tabWidth": 2, + "printWidth": 100, "singleQuote": true, "trailingComma": "all", "importOrder": ["^@abacus-network/(.*)$", "^../(.*)$", "^./(.*)$"], diff --git a/src/components/animation/Spinner.tsx b/src/components/animation/Spinner.tsx index 263f617..d6b58fe 100644 --- a/src/components/animation/Spinner.tsx +++ b/src/components/animation/Spinner.tsx @@ -5,9 +5,7 @@ import styles from './Spinner.module.css'; // From https://loading.io/css/ function _Spinner({ white, classes }: { white?: boolean; classes?: string }) { return ( -
+
diff --git a/src/components/buttons/ConnectAwareSubmitButton.tsx b/src/components/buttons/ConnectAwareSubmitButton.tsx index c37838d..6b034db 100644 --- a/src/components/buttons/ConnectAwareSubmitButton.tsx +++ b/src/components/buttons/ConnectAwareSubmitButton.tsx @@ -19,19 +19,13 @@ export function ConnectAwareSubmitButton(props: Props) { const isAccountReady = !!(address && isConnected && connector); - const { errors, setErrors, touched, setTouched } = - useFormikContext(); + const { errors, setErrors, touched, setTouched } = useFormikContext(); - const hasError = - Object.keys(touched).length > 0 && Object.keys(errors).length > 0; + const hasError = Object.keys(touched).length > 0 && Object.keys(errors).length > 0; const firstError = `${Object.values(errors)[0]}` || 'Unknown error'; const color = hasError ? 'red' : 'blue'; - const text = hasError - ? firstError - : isAccountReady - ? connectText - : 'Connect Wallet'; + const text = hasError ? firstError : isAccountReady ? connectText : 'Connect Wallet'; const type = isAccountReady ? 'submit' : 'button'; const onClick = isAccountReady ? undefined : openConnectModal; diff --git a/src/components/buttons/IconButton.tsx b/src/components/buttons/IconButton.tsx index d76e27e..a06d5a8 100644 --- a/src/components/buttons/IconButton.tsx +++ b/src/components/buttons/IconButton.tsx @@ -13,16 +13,7 @@ export interface IconButtonProps { } export function IconButton(props: PropsWithChildren) { - const { - width, - height, - classes, - onClick, - imgSrc, - disabled, - title, - passThruProps, - } = props; + const { width, height, classes, onClick, imgSrc, disabled, title, passThruProps } = props; const base = 'flex items-center justify-center transition-all'; const onHover = 'hover:opacity-70'; diff --git a/src/components/buttons/SolidButton.tsx b/src/components/buttons/SolidButton.tsx index 4b6c682..7f2bcd0 100644 --- a/src/components/buttons/SolidButton.tsx +++ b/src/components/buttons/SolidButton.tsx @@ -28,8 +28,7 @@ export function SolidButton(props: PropsWithChildren) { } = props; const color = _color ?? 'blue'; - const base = - 'flex items-center justify-center rounded-full transition-all duration-1000'; + const base = 'flex items-center justify-center rounded-full transition-all duration-1000'; const sizing = sizeToClasses(size); let baseColors, onHover, onActive; if (color === 'blue') { diff --git a/src/components/icons/ChainIcon.tsx b/src/components/icons/ChainIcon.tsx index 45cd1de..2ea9b2c 100644 --- a/src/components/icons/ChainIcon.tsx +++ b/src/components/icons/ChainIcon.tsx @@ -2,11 +2,7 @@ import Image from 'next/future/image'; import { memo } from 'react'; import { chain } from 'wagmi'; -import { - avalancheChain, - bscChain, - celoMainnetChain, -} from '../../consts/networksConfig'; +import { avalancheChain, bscChain, celoMainnetChain } from '../../consts/networksConfig'; import QuestionMark from '../../images/icons/question-mark.svg'; import Arbitrum from '../../images/logos/arbitrum.svg'; import Avalanche from '../../images/logos/avalanche.svg'; @@ -27,13 +23,7 @@ const CHAIN_TO_ICON = { [chain.polygon.id]: Polygon, }; -function _ChainIcon({ - chainId, - size = 44, -}: { - chainId?: number; - size?: number; -}) { +function _ChainIcon({ chainId, size = 44 }: { chainId?: number; size?: number }) { const imageSrc = (chainId && CHAIN_TO_ICON[chainId]) || QuestionMark; return ( diff --git a/src/components/layout/ContentFrame.tsx b/src/components/layout/ContentFrame.tsx index 95fa8b1..cfa7547 100644 --- a/src/components/layout/ContentFrame.tsx +++ b/src/components/layout/ContentFrame.tsx @@ -1,10 +1,6 @@ import { PropsWithChildren } from 'react'; -import { - BackgroundBanner, - BannerColorContext, - useBackgroundBannerState, -} from './BackgroundBanner'; +import { BackgroundBanner, BannerColorContext, useBackgroundBannerState } from './BackgroundBanner'; export function ContentFrame(props: PropsWithChildren) { // Provide context so children can change banner color @@ -12,10 +8,7 @@ export function ContentFrame(props: PropsWithChildren) { return (
- @@ -45,20 +43,10 @@ export function Header() { Transaction Debugger - + Docs - + About @@ -70,25 +58,14 @@ export function Header() {
{/* Dropdown menu, used on mobile */} -
+
- + - + @@ -120,10 +97,7 @@ export function Header() { target="_blank" rel="noopener noreferrer" > - +
@@ -143,27 +117,17 @@ function NetworkSelector() { const { buttonProps, itemProps, isOpen, setIsOpen } = useDropdownMenu(2); return (
- -
+
setIsOpen(false)} - href={ - config.environment !== Environment.Mainnet - ? allConfigs.mainnet.url - : undefined - } + href={config.environment !== Environment.Mainnet ? allConfigs.mainnet.url : undefined} target="_blank" rel="noopener noreferrer" > @@ -175,11 +139,7 @@ function NetworkSelector() { className={`${styles.dropdownOption} justify-center ${ config.environment === Environment.Testnet2 && styles.activeEnv }`} - href={ - config.environment !== Environment.Testnet2 - ? allConfigs.testnet2.url - : undefined - } + href={config.environment !== Environment.Testnet2 ? allConfigs.testnet2.url : undefined} target="_blank" rel="noopener noreferrer" > diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx index 6733e65..a45939e 100644 --- a/src/components/search/SearchBar.tsx +++ b/src/components/search/SearchBar.tsx @@ -29,9 +29,7 @@ export function SearchBar({ value, onChangeValue, fetching }: Props) { />
{fetching && } - {!fetching && !value && ( - - )} + {!fetching && !value && } {!fetching && value && (
-
- {text} -
+
{text}
@@ -45,13 +43,7 @@ export function SearchInvalidError({ show }: { show: boolean }) { ); } -export function SearchEmptyError({ - show, - hasInput, -}: { - show: boolean; - hasInput: boolean; -}) { +export function SearchEmptyError({ show, hasInput }: { show: boolean; hasInput: boolean }) { return ( >( - (result, chain) => { - result[chain.id] = chain; - return result; - }, - {}, -); +export const chainIdToChain = allChains.reduce>((result, chain) => { + result[chain.id] = chain; + return result; +}, {}); diff --git a/src/features/search/MessageDetails.tsx b/src/features/search/MessageDetails.tsx index db559bc..41c5557 100644 --- a/src/features/search/MessageDetails.tsx +++ b/src/features/search/MessageDetails.tsx @@ -62,14 +62,7 @@ export function MessageDetails({ messageId }: { messageId: string }) { } else if (bannerClassName) { setBannerClassName(''); } - }, [ - error, - fetching, - message, - isMessageFound, - bannerClassName, - setBannerClassName, - ]); + }, [error, fetching, message, isMessageFound, bannerClassName, setBannerClassName]); const reExecutor = useCallback(() => { if (!isMessageFound || status !== MessageStatus.Delivered) { @@ -174,10 +167,7 @@ function TransactionCard({ help, shouldBlur, }: TransactionCardProps) { - const txExplorerLink = getTxExplorerLink( - chainId, - transaction?.transactionHash, - ); + const txExplorerLink = getTxExplorerLink(chainId, transaction?.transactionHash); return (
@@ -194,9 +184,7 @@ function TransactionCard({ @@ -219,9 +207,7 @@ function TransactionCard({ @@ -271,15 +257,10 @@ function DetailsCard({
- +
-

- Message Details -

+

Message Details

@@ -333,24 +314,16 @@ function ValueRow({ return (
- + {display} - {showCopy && ( - - )} + {showCopy && }
); } function ErrorIcon() { - return ( - - ); + return ; } const messageDetailsQuery = ` @@ -410,10 +383,8 @@ query MessageDetails ($messageId: bigint!){ }`; const helpText = { - origin: - 'Info about the transaction that initiated the message placement into the outbox.', + origin: 'Info about the transaction that initiated the message placement into the outbox.', destination: 'Info about the transaction that triggered the delivery of the message from an inbox.', - details: - 'Immutable information about the message itself such as its contents.', + details: 'Immutable information about the message itself such as its contents.', }; diff --git a/src/features/search/MessageSearch.tsx b/src/features/search/MessageSearch.tsx index 2af558b..ecb2ec2 100644 --- a/src/features/search/MessageSearch.tsx +++ b/src/features/search/MessageSearch.tsx @@ -47,9 +47,7 @@ export function MessageSearch() { const debouncedSearchInput = useDebounce(searchInput, 750); const hasInput = !!debouncedSearchInput; const sanitizedInput = sanitizeString(debouncedSearchInput); - const isValidInput = hasInput - ? isValidSearchQuery(sanitizedInput, true) - : true; + const isValidInput = hasInput ? isValidSearchQuery(sanitizedInput, true) : true; // Filter state and handlers const chainOptions = useMemo(getChainOptionList, []); @@ -85,37 +83,21 @@ export function MessageSearch() { return ( <> - +
{/* Content header and filter bar */}
-

- {!hasInput ? 'Latest Messages' : 'Search Results'} -

+

{!hasInput ? 'Latest Messages' : 'Search Results'}

- + - +
@@ -163,11 +143,7 @@ function getChainOptionList(): Array<{ value: string; display: string }> { ]; } -function assembleQuery( - searchInput: string, - originFilter: string, - destFilter: string, -) { +function assembleQuery(searchInput: string, originFilter: string, destFilter: string) { const hasInput = !!searchInput; const variables = { search: hasInput ? trimLeading0x(searchInput) : undefined, diff --git a/src/features/search/MessageSummary.tsx b/src/features/search/MessageSummary.tsx index 28add21..620a693 100644 --- a/src/features/search/MessageSummary.tsx +++ b/src/features/search/MessageSummary.tsx @@ -6,15 +6,7 @@ import { shortenAddress } from '../../utils/addresses'; import { getHumanReadableTimeString } from '../../utils/time'; export function MessageSummary({ message }: { message: MessageStub }) { - const { - id, - status, - sender, - recipient, - timestamp, - originChainId, - destinationChainId, - } = message; + const { id, status, sender, recipient, timestamp, originChainId, destinationChainId } = message; let statusColor = 'bg-beige-500'; let statusText = 'Pending'; @@ -29,33 +21,22 @@ export function MessageSummary({ message }: { message: MessageStub }) { return (
- +
Sender
-
- {shortenAddress(sender) || 'Invalid Address'} -
+
{shortenAddress(sender) || 'Invalid Address'}
Recipient
-
- {shortenAddress(recipient) || 'Invalid Address'} -
+
{shortenAddress(recipient) || 'Invalid Address'}
Time sent
-
- {getHumanReadableTimeString(timestamp)} -
+
{getHumanReadableTimeString(timestamp)}
-
+
{statusText}
diff --git a/src/features/search/placeholderMessages.ts b/src/features/search/placeholderMessages.ts index 37fca78..0a355a6 100644 --- a/src/features/search/placeholderMessages.ts +++ b/src/features/search/placeholderMessages.ts @@ -1,15 +1,10 @@ import { constants } from 'ethers'; import { chain } from 'wagmi'; -import { - avalancheChain, - bscChain, - celoMainnetChain, -} from '../../consts/networksConfig'; +import { avalancheChain, bscChain, celoMainnetChain } from '../../consts/networksConfig'; import { Message, MessageStatus, PartialTransactionReceipt } from '../../types'; -const TX_HASH_ZERO = - '0x0000000000000000000000000000000000000000000000000000000000000000'; +const TX_HASH_ZERO = '0x0000000000000000000000000000000000000000000000000000000000000000'; export const TX_ZERO: PartialTransactionReceipt = { from: constants.AddressZero, diff --git a/src/features/search/query.ts b/src/features/search/query.ts index 3c6a6a8..35ceaa8 100644 --- a/src/features/search/query.ts +++ b/src/features/search/query.ts @@ -1,10 +1,5 @@ import { domainToChain } from '../../consts/domains'; -import { - Message, - MessageStatus, - MessageStub, - PartialTransactionReceipt, -} from '../../types'; +import { Message, MessageStatus, MessageStub, PartialTransactionReceipt } from '../../types'; import { ensureLeading0x } from '../../utils/addresses'; import { logger } from '../../utils/logger'; @@ -16,18 +11,12 @@ import { TransactionEntry, } from './types'; -export function parseMessageStubResult( - data: MessagesStubQueryResult | undefined, -): MessageStub[] { +export function parseMessageStubResult(data: MessagesStubQueryResult | undefined): MessageStub[] { if (!data?.message?.length) return []; - return data.message - .map(parseMessageStub) - .filter((m): m is MessageStub => !!m); + return data.message.map(parseMessageStub).filter((m): m is MessageStub => !!m); } -export function parseMessageQueryResult( - data: MessagesQueryResult | undefined, -): Message[] { +export function parseMessageQueryResult(data: MessagesQueryResult | undefined): Message[] { if (!data?.message?.length) return []; return data.message.map(parseMessage).filter((m): m is Message => !!m); } diff --git a/src/features/search/utils.ts b/src/features/search/utils.ts index 44ead73..e08f689 100644 --- a/src/features/search/utils.ts +++ b/src/features/search/utils.ts @@ -1,8 +1,5 @@ import { chainIdToChain } from '../../consts/networksConfig'; -import { - isValidAddressFast, - isValidTransactionHash, -} from '../../utils/addresses'; +import { isValidAddressFast, isValidTransactionHash } from '../../utils/addresses'; export function isValidSearchQuery(input: string, allowAddress?: boolean) { if (!input) return false; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index a9b797f..ed00b00 100755 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -8,15 +8,8 @@ import '@rainbow-me/rainbowkit/styles.css'; import type { AppProps } from 'next/app'; import { ToastContainer, Zoom, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; -import { - Provider as UrqlProvider, - createClient as createUrqlClient, -} from 'urql'; -import { - WagmiConfig, - configureChains, - createClient as createWagmiClient, -} from 'wagmi'; +import { Provider as UrqlProvider, createClient as createUrqlClient } from 'urql'; +import { WagmiConfig, configureChains, createClient as createWagmiClient } from 'wagmi'; import { publicProvider } from 'wagmi/providers/public'; import { ErrorBoundary } from '../components/errors/ErrorBoundary'; @@ -78,11 +71,7 @@ export default function App({ Component, router, pageProps }: AppProps) { - + ); diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index b8d1920..989ba74 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -6,23 +6,9 @@ export default function Document() { - - - + + + @@ -30,10 +16,7 @@ export default function Document() { - + - + { useEffect(() => { if (!messageId || typeof messageId !== 'string') - router - .replace('/') - .catch((e) => logger.error('Error routing back to home', e)); + router.replace('/').catch((e) => logger.error('Error routing back to home', e)); }, [router, messageId]); if (!messageId || typeof messageId !== 'string') return null; diff --git a/src/styles/globals.css b/src/styles/globals.css index 8b067b4..8bf65cd 100755 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -145,8 +145,7 @@ Dropdowns padding: 0.5rem 0.6rem; border: 1px solid #f1f1f1; border-radius: 0.5rem; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), - 0 2px 4px -1px rgba(0, 0, 0, 0.06); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); transition: transform 0.2s ease, opacity 0.2s ease, visibility 0s linear 0.2s; will-change: transform; } diff --git a/src/utils/addresses.ts b/src/utils/addresses.ts index a9e9f32..9520f2e 100644 --- a/src/utils/addresses.ts +++ b/src/utils/addresses.ts @@ -37,9 +37,7 @@ export function shortenAddress(address: string, capitalize?: boolean) { try { const normalized = normalizeAddress(address); const shortened = - normalized.substring(0, 6) + - '...' + - normalized.substring(normalized.length - 4); + normalized.substring(0, 6) + '...' + normalized.substring(normalized.length - 4); return capitalize ? capitalizeAddress(shortened) : shortened; } catch (error) { logger.error('Unable to shorten invalid address', address, error); diff --git a/src/utils/objects.ts b/src/utils/objects.ts index a6dbc9b..8fa0cf7 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -1,5 +1,3 @@ export function invertKeysAndValues(data: any) { - return Object.fromEntries( - Object.entries(data).map(([key, value]) => [value, key]), - ); + return Object.fromEntries(Object.entries(data).map(([key, value]) => [value, key])); } diff --git a/src/utils/string.ts b/src/utils/string.ts index e0daaf7..1555a9d 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -15,7 +15,5 @@ export function sanitizeString(str: string) { export function trimToLength(value: string, maxLength: number) { if (!value) return ''; const trimmed = value.trim(); - return trimmed.length > maxLength - ? trimmed.substring(0, maxLength) + '...' - : trimmed; + return trimmed.length > maxLength ? trimmed.substring(0, maxLength) + '...' : trimmed; } diff --git a/src/utils/timeout.ts b/src/utils/timeout.ts index 0663d7c..e6876fa 100644 --- a/src/utils/timeout.ts +++ b/src/utils/timeout.ts @@ -67,17 +67,12 @@ export async function fetchWithTimeout( } export function sleep(milliseconds: number) { - return new Promise((resolve) => - setTimeout(() => resolve(true), milliseconds), - ); + return new Promise((resolve) => setTimeout(() => resolve(true), milliseconds)); } export const PROMISE_TIMEOUT = '__promise_timeout__'; -export async function promiseTimeout( - promise: Promise, - milliseconds: number, -) { +export async function promiseTimeout(promise: Promise, milliseconds: number) { // Create a promise that rejects in milliseconds const timeout = new Promise((_resolve, reject) => { const id = setTimeout(() => { From f21fe4b15b1e34bb19cc3b04e5aa9ce70299c7a3 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Fri, 23 Sep 2022 18:56:33 -0400 Subject: [PATCH 03/11] Progress on tx debugger UI --- src/components/search/SearchBar.tsx | 5 +-- src/features/debugger/TxDebugger.tsx | 50 +++++++++++++++++++++++++- src/features/search/MessageDetails.tsx | 2 +- src/features/search/MessageSearch.tsx | 9 +++-- src/features/search/utils.ts | 16 --------- src/utils/explorers.ts | 16 +++++++++ 6 files changed, 75 insertions(+), 23 deletions(-) create mode 100644 src/utils/explorers.ts diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx index a45939e..f7bcfb8 100644 --- a/src/components/search/SearchBar.tsx +++ b/src/components/search/SearchBar.tsx @@ -8,11 +8,12 @@ import { IconButton } from '../buttons/IconButton'; interface Props { value: string; + placeholder: string; onChangeValue: (v: string) => void; fetching: boolean; } -export function SearchBar({ value, onChangeValue, fetching }: Props) { +export function SearchBar({ value, placeholder, onChangeValue, fetching }: Props) { const onChange = (event: ChangeEvent | null) => { const value = event?.target?.value || ''; onChangeValue(value); @@ -24,7 +25,7 @@ export function SearchBar({ value, onChangeValue, fetching }: Props) { value={value} onChange={onChange} type="text" - placeholder="Search for messages by address or transaction hash" + placeholder={placeholder} className="p-2 sm:px-4 md:px-5 flex-1 h-10 sm:h-12 rounded focus:outline-none" />
diff --git a/src/features/debugger/TxDebugger.tsx b/src/features/debugger/TxDebugger.tsx index aa17c16..dd0a96b 100644 --- a/src/features/debugger/TxDebugger.tsx +++ b/src/features/debugger/TxDebugger.tsx @@ -1,3 +1,51 @@ +import { useState } from 'react'; + +import { Fade } from '../../components/animation/Fade'; +import { SearchBar } from '../../components/search/SearchBar'; +import { + SearchEmptyError, + SearchInvalidError, + SearchUnknownError, +} from '../../components/search/SearchError'; +import useDebounce from '../../utils/debounce'; +import { sanitizeString } from '../../utils/string'; +import { isValidSearchQuery } from '../search/utils'; + export function TxDebugger() { - return
TODO
; + // Search text input + const [searchInput, setSearchInput] = useState(''); + const debouncedSearchInput = useDebounce(searchInput, 750); + const hasInput = !!debouncedSearchInput; + const sanitizedInput = sanitizeString(debouncedSearchInput); + const isValidInput = hasInput ? isValidSearchQuery(sanitizedInput, false) : true; + + const fetching = false; + const hasError = false; + const txResult = {}; + + return ( + <> + +
+ {/* Content header and filter bar */} +
+

{!hasInput ? 'Transaction Debugger' : 'Search Result'}

+
+ {/* Message list */} + {JSON.stringify(txResult)} + + + + +
+ + ); } diff --git a/src/features/search/MessageDetails.tsx b/src/features/search/MessageDetails.tsx index 41c5557..3f9afc2 100644 --- a/src/features/search/MessageDetails.tsx +++ b/src/features/search/MessageDetails.tsx @@ -15,6 +15,7 @@ import CheckmarkIcon from '../../images/icons/checkmark-circle.svg'; import ErrorCircleIcon from '../../images/icons/error-circle.svg'; import { MessageStatus, PartialTransactionReceipt } from '../../types'; import { getChainName } from '../../utils/chains'; +import { getTxExplorerLink } from '../../utils/explorers'; import { logger } from '../../utils/logger'; import { getDateTimeString } from '../../utils/time'; import { useInterval } from '../../utils/timeout'; @@ -22,7 +23,6 @@ import { useInterval } from '../../utils/timeout'; import { PLACEHOLDER_MESSAGES } from './placeholderMessages'; import { parseMessageQueryResult } from './query'; import { MessagesQueryResult } from './types'; -import { getTxExplorerLink } from './utils'; const AUTO_REFRESH_DELAY = 10000; diff --git a/src/features/search/MessageSearch.tsx b/src/features/search/MessageSearch.tsx index ecb2ec2..520ec52 100644 --- a/src/features/search/MessageSearch.tsx +++ b/src/features/search/MessageSearch.tsx @@ -83,7 +83,12 @@ export function MessageSearch() { return ( <> - +
{/* Content header and filter bar */}
@@ -121,9 +126,7 @@ export function MessageSearch() { - - Date: Fri, 23 Sep 2022 21:52:57 -0400 Subject: [PATCH 04/11] Setup zustand store Improve header presentation --- package.json | 3 ++- src/components/layout/AppLayout.tsx | 2 +- src/components/layout/BackgroundBanner.tsx | 22 +++------------- src/components/layout/ContentFrame.tsx | 11 +++----- src/components/nav/Header.tsx | 29 +++++++++------------- src/features/search/MessageDetails.tsx | 22 +++++++++------- src/store.ts | 19 ++++++++++++++ yarn.lock | 18 ++++++++++++++ 8 files changed, 71 insertions(+), 55 deletions(-) create mode 100644 src/store.ts diff --git a/package.json b/package.json index 0c40c37..dee0c10 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "react-dom": "^18.2.0", "react-toastify": "^9.0.5", "urql": "^3.0.1", - "wagmi": "^0.5.11" + "wagmi": "^0.5.11", + "zustand": "^4.1.1" }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^3.2.0", diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx index 46d7e7a..fd0fa76 100644 --- a/src/components/layout/AppLayout.tsx +++ b/src/components/layout/AppLayout.tsx @@ -19,7 +19,7 @@ export function AppLayout({ pathName, children }: PropsWithChildren) {
-
+
{children}
diff --git a/src/components/layout/BackgroundBanner.tsx b/src/components/layout/BackgroundBanner.tsx index 2d9be0c..7ad3cb1 100644 --- a/src/components/layout/BackgroundBanner.tsx +++ b/src/components/layout/BackgroundBanner.tsx @@ -1,29 +1,13 @@ -import { createContext, useContext, useMemo, useState } from 'react'; - +import { useStore } from '../../store'; import { classNameToColor } from '../../styles/Color'; import { WideChevronIcon } from '../icons/WideChevron'; -export const BannerColorContext = createContext<{ - bannerClassName: string; - setBannerClassName?: (name: string) => void; -}>({ bannerClassName: '', setBannerClassName: undefined }); - -export function useBackgroundBannerState() { - // State for managing banner class, to be used as context value - const [bannerClassName, setBannerClassName] = useState(''); - const bannerState = useMemo( - () => ({ bannerClassName, setBannerClassName }), - [bannerClassName, setBannerClassName], - ); - return bannerState; -} - export function useBackgroundBanner() { - return useContext(BannerColorContext); + return useStore((s) => s.bannerClassName); } export function BackgroundBanner() { - const { bannerClassName } = useBackgroundBanner(); + const bannerClassName = useStore((s) => s.bannerClassName); const colorClass = bannerClassName || 'bg-blue-500'; return ( diff --git a/src/components/layout/ContentFrame.tsx b/src/components/layout/ContentFrame.tsx index cfa7547..e05af34 100644 --- a/src/components/layout/ContentFrame.tsx +++ b/src/components/layout/ContentFrame.tsx @@ -1,18 +1,13 @@ import { PropsWithChildren } from 'react'; -import { BackgroundBanner, BannerColorContext, useBackgroundBannerState } from './BackgroundBanner'; +import { BackgroundBanner } from './BackgroundBanner'; export function ContentFrame(props: PropsWithChildren) { - // Provide context so children can change banner color - const bannerState = useBackgroundBannerState(); - return (
- - -
{props.children}
-
+ +
{props.children}
); diff --git a/src/components/nav/Header.tsx b/src/components/nav/Header.tsx index 75a8740..92b25dd 100644 --- a/src/components/nav/Header.tsx +++ b/src/components/nav/Header.tsx @@ -13,7 +13,7 @@ import InfoIcon from '../../images/icons/info-circle.svg'; import Logo from '../../images/logos/hyperlane-logo.svg'; import Name from '../../images/logos/hyperlane-name.svg'; -export function Header() { +export function Header({ pathName }: { pathName: string }) { const { buttonProps, itemProps, isOpen, setIsOpen } = useDropdownMenu(5); const closeDropdown = () => { setIsOpen(false); @@ -21,31 +21,26 @@ export function Header() { const isMainnet = config.environment === Environment.Mainnet; return ( -
+
-
- -
-
- Hyperlane -
-
Explorer
+ + Hyperlane +
Explorer
-
+
- Home + Home - Transaction Debugger + + Debugger + - - Docs - About @@ -66,7 +61,7 @@ export function Header() { - + s.setBanner); useEffect(() => { - if (!setBannerClassName || fetching) return; + if (fetching) return; if (error) { logger.error('Error fetching message details', error); toast.error(`Error fetching message: ${error.message?.substring(0, 30)}`); - setBannerClassName('bg-red-600'); + setBanner('bg-red-600'); } else if (message.status === MessageStatus.Failing) { - setBannerClassName('bg-red-600'); + setBanner('bg-red-600'); } else if (!isMessageFound) { - setBannerClassName('bg-gray-500'); - } else if (bannerClassName) { - setBannerClassName(''); + setBanner('bg-gray-500'); + } else { + setBanner(''); } - }, [error, fetching, message, isMessageFound, bannerClassName, setBannerClassName]); + }, [error, fetching, message, isMessageFound, setBanner]); + + useEffect(() => { + return () => setBanner(''); + }, [setBanner]); const reExecutor = useCallback(() => { if (!isMessageFound || status !== MessageStatus.Delivered) { diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..0c9c751 --- /dev/null +++ b/src/store.ts @@ -0,0 +1,19 @@ +import create from 'zustand'; + +import { Environment } from './consts/appConfig'; + +// Keeping everything here for now as state is simple +// Will refactor into slices as necessary +interface AppState { + environment: Environment; + setEnvironment: (env: Environment) => void; + bannerClassName: string; + setBanner: (env: string) => void; +} + +export const useStore = create()((set) => ({ + environment: Environment.Mainnet, + setEnvironment: (env: Environment) => set(() => ({ environment: env })), + bannerClassName: '', + setBanner: (className: string) => set(() => ({ bannerClassName: className })), +})); diff --git a/yarn.lock b/yarn.lock index 7d85819..449037d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -937,6 +937,7 @@ __metadata: typescript: ^4.7.4 urql: ^3.0.1 wagmi: ^0.5.11 + zustand: ^4.1.1 languageName: unknown linkType: soft @@ -7179,3 +7180,20 @@ __metadata: checksum: d4506fa171a9b2eab14071590903150d8b2c4f8b97b71c08cbc63da172a1b79fee9bcdf33833b6242ba5cee302430013437892bb02b2534dfe1622d9fd11790e languageName: node linkType: hard + +"zustand@npm:^4.1.1": + version: 4.1.1 + resolution: "zustand@npm:4.1.1" + dependencies: + use-sync-external-store: 1.2.0 + peerDependencies: + immer: ">=9.0" + react: ">=16.8" + peerDependenciesMeta: + immer: + optional: true + react: + optional: true + checksum: 03eefb193e2ecb43a761c81cb60f517c2780289dab0f55f2cbdb91400924c6291abb5a007b32ee19787b8f72b6769f891a38b64fd296660c170d632c3182f6e1 + languageName: node + linkType: hard From 7befbeb0b7b2ad23f0c1840c42d283a2bc71b36e Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 24 Sep 2022 18:30:06 -0400 Subject: [PATCH 05/11] Convert network selector into select field --- src/components/nav/Header.tsx | 65 +++++++++++------------------------ src/consts/appConfig.ts | 12 +++---- src/pages/_app.tsx | 20 +++++++---- 3 files changed, 39 insertions(+), 58 deletions(-) diff --git a/src/components/nav/Header.tsx b/src/components/nav/Header.tsx index 92b25dd..fcce259 100644 --- a/src/components/nav/Header.tsx +++ b/src/components/nav/Header.tsx @@ -2,23 +2,23 @@ import Image from 'next/future/image'; import Link from 'next/link'; import useDropdownMenu from 'react-accessible-dropdown-menu-hook'; -import { Environment, allConfigs, config } from '../../consts/appConfig'; +import { Environment } from '../../consts/appConfig'; import { links } from '../../consts/links'; import BookIcon from '../../images/icons/book.svg'; import BugIcon from '../../images/icons/bug.svg'; import HamburgerIcon from '../../images/icons/hamburger.svg'; import HouseIcon from '../../images/icons/house.svg'; -import HubIcon from '../../images/icons/hub.svg'; import InfoIcon from '../../images/icons/info-circle.svg'; import Logo from '../../images/logos/hyperlane-logo.svg'; import Name from '../../images/logos/hyperlane-name.svg'; +import { useStore } from '../../store'; +import { SelectField } from '../input/SelectField'; export function Header({ pathName }: { pathName: string }) { - const { buttonProps, itemProps, isOpen, setIsOpen } = useDropdownMenu(5); + const { buttonProps, itemProps, isOpen, setIsOpen } = useDropdownMenu(4); const closeDropdown = () => { setIsOpen(false); }; - const isMainnet = config.environment === Environment.Mainnet; return (
@@ -84,16 +84,6 @@ export function Header({ pathName }: { pathName: string }) { > - - -
); @@ -109,42 +99,29 @@ function DropdownItemContent({ icon, text }: { icon: any; text: string }) { } function NetworkSelector() { - const { buttonProps, itemProps, isOpen, setIsOpen } = useDropdownMenu(2); + const { environment, setEnvironment } = useStore((s) => ({ + environment: s.environment, + setEnvironment: s.setEnvironment, + })); + return ( ); } +const envOptions = [ + { value: Environment.Mainnet, display: 'Mainnet' }, + { value: Environment.Testnet2, display: 'Testnet' }, +]; + const styles = { navLink: 'flex items-center tracking-wide text-gray-600 text-[0.95rem] hover:underline hover:opacity-70 decoration-2 underline-offset-[6px] transition-all', diff --git a/src/consts/appConfig.ts b/src/consts/appConfig.ts index 607bae4..24dbc7d 100644 --- a/src/consts/appConfig.ts +++ b/src/consts/appConfig.ts @@ -3,18 +3,16 @@ export enum Environment { Testnet2 = 'testnet2', } -// Toggle for testnet2 vs mainnet -const environment: Environment = Environment.Mainnet; const isDevMode = process?.env?.NODE_ENV === 'development'; const version = process?.env?.NEXT_PUBLIC_VERSION ?? null; -export const allConfigs: Record = { +export const configs: Record = { mainnet: { name: 'Hyperlane Explorer', environment: Environment.Mainnet, debug: isDevMode, version, - url: 'https://hyperlane-explorer.vercel.app/', + url: 'https://explorer.hyperlane.xyz', apiUrl: 'https://abacus-explorer-api.hasura.app/v1/graphql', }, testnet2: { @@ -22,13 +20,11 @@ export const allConfigs: Record = { environment: Environment.Testnet2, debug: true, version, - url: 'TODO', - apiUrl: 'TODO', + url: 'https://explorer.hyperlane.xyz', + apiUrl: 'https://abacus-explorer-api.hasura.app/v1/graphql', // TODO change }, }; -export const config = Object.freeze(allConfigs[environment]); - interface Config { name: string; environment: Environment; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index ed00b00..8a96908 100755 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -8,14 +8,15 @@ import '@rainbow-me/rainbowkit/styles.css'; import type { AppProps } from 'next/app'; import { ToastContainer, Zoom, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; -import { Provider as UrqlProvider, createClient as createUrqlClient } from 'urql'; +import { Client, Provider as UrqlProvider, createClient as createUrqlClient } from 'urql'; import { WagmiConfig, configureChains, createClient as createWagmiClient } from 'wagmi'; import { publicProvider } from 'wagmi/providers/public'; import { ErrorBoundary } from '../components/errors/ErrorBoundary'; import { AppLayout } from '../components/layout/AppLayout'; -import { config } from '../consts/appConfig'; +import { Environment, configs } from '../consts/appConfig'; import { prodChains } from '../consts/networksConfig'; +import { useStore } from '../store'; import { Color } from '../styles/Color'; import '../styles/fonts.css'; import '../styles/globals.css'; @@ -41,11 +42,18 @@ const wagmiClient = createWagmiClient({ connectors, }); -const urqlClient = createUrqlClient({ - url: config.apiUrl, -}); +const urqlClients: Record = { + [Environment.Mainnet]: createUrqlClient({ + url: configs.mainnet.apiUrl, + }), + [Environment.Testnet2]: createUrqlClient({ + url: configs.testnet2.apiUrl, + }), +}; export default function App({ Component, router, pageProps }: AppProps) { + const environment = useStore((s) => s.environment); + // Disable app SSR for now as it's not needed and // complicates graphql integration const isSsr = useIsSsr(); @@ -65,7 +73,7 @@ export default function App({ Component, router, pageProps }: AppProps) { fontStack: 'system', })} > - + From d0e5baa30bb6f628e0514809bba0fde5c5365647 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 24 Sep 2022 18:42:11 -0400 Subject: [PATCH 06/11] Refactor env enum into separate file --- src/components/nav/Header.tsx | 6 +++--- src/consts/appConfig.ts | 8 +------- src/consts/environments.ts | 9 +++++++++ src/features/debugger/TxDebugger.tsx | 6 +++++- src/pages/_app.tsx | 3 ++- src/store.ts | 2 +- 6 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 src/consts/environments.ts diff --git a/src/components/nav/Header.tsx b/src/components/nav/Header.tsx index fcce259..470c9d7 100644 --- a/src/components/nav/Header.tsx +++ b/src/components/nav/Header.tsx @@ -2,7 +2,7 @@ import Image from 'next/future/image'; import Link from 'next/link'; import useDropdownMenu from 'react-accessible-dropdown-menu-hook'; -import { Environment } from '../../consts/appConfig'; +import { Environment, envDisplayValue } from '../../consts/environments'; import { links } from '../../consts/links'; import BookIcon from '../../images/icons/book.svg'; import BugIcon from '../../images/icons/bug.svg'; @@ -118,8 +118,8 @@ function NetworkSelector() { } const envOptions = [ - { value: Environment.Mainnet, display: 'Mainnet' }, - { value: Environment.Testnet2, display: 'Testnet' }, + { value: Environment.Mainnet, display: envDisplayValue[Environment.Mainnet] }, + { value: Environment.Testnet2, display: envDisplayValue[Environment.Testnet2] }, ]; const styles = { diff --git a/src/consts/appConfig.ts b/src/consts/appConfig.ts index 24dbc7d..170c6b2 100644 --- a/src/consts/appConfig.ts +++ b/src/consts/appConfig.ts @@ -1,14 +1,10 @@ -export enum Environment { - Mainnet = 'mainnet', - Testnet2 = 'testnet2', -} +import { Environment } from './environments'; const isDevMode = process?.env?.NODE_ENV === 'development'; const version = process?.env?.NEXT_PUBLIC_VERSION ?? null; export const configs: Record = { mainnet: { - name: 'Hyperlane Explorer', environment: Environment.Mainnet, debug: isDevMode, version, @@ -16,7 +12,6 @@ export const configs: Record = { apiUrl: 'https://abacus-explorer-api.hasura.app/v1/graphql', }, testnet2: { - name: 'Hyperlane Testnet Explorer', environment: Environment.Testnet2, debug: true, version, @@ -26,7 +21,6 @@ export const configs: Record = { }; interface Config { - name: string; environment: Environment; debug: boolean; version: string | null; diff --git a/src/consts/environments.ts b/src/consts/environments.ts new file mode 100644 index 0000000..ae4c6cb --- /dev/null +++ b/src/consts/environments.ts @@ -0,0 +1,9 @@ +export enum Environment { + Mainnet = 'mainnet', + Testnet2 = 'testnet2', +} + +export const envDisplayValue = { + [Environment.Mainnet]: 'Mainnet', + [Environment.Testnet2]: 'Testnet', +}; diff --git a/src/features/debugger/TxDebugger.tsx b/src/features/debugger/TxDebugger.tsx index dd0a96b..f4b37d7 100644 --- a/src/features/debugger/TxDebugger.tsx +++ b/src/features/debugger/TxDebugger.tsx @@ -7,11 +7,15 @@ import { SearchInvalidError, SearchUnknownError, } from '../../components/search/SearchError'; +import { envDisplayValue } from '../../consts/environments'; +import { useStore } from '../../store'; import useDebounce from '../../utils/debounce'; import { sanitizeString } from '../../utils/string'; import { isValidSearchQuery } from '../search/utils'; export function TxDebugger() { + const environment = useStore((s) => s.environment); + // Search text input const [searchInput, setSearchInput] = useState(''); const debouncedSearchInput = useDebounce(searchInput, 750); @@ -34,7 +38,7 @@ export function TxDebugger() {
{/* Content header and filter bar */}
-

{!hasInput ? 'Transaction Debugger' : 'Search Result'}

+

{`Transaction Debugger (${envDisplayValue[environment]})`}

{/* Message list */} {JSON.stringify(txResult)} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 8a96908..5227f31 100755 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -14,7 +14,8 @@ import { publicProvider } from 'wagmi/providers/public'; import { ErrorBoundary } from '../components/errors/ErrorBoundary'; import { AppLayout } from '../components/layout/AppLayout'; -import { Environment, configs } from '../consts/appConfig'; +import { configs } from '../consts/appConfig'; +import { Environment } from '../consts/environments'; import { prodChains } from '../consts/networksConfig'; import { useStore } from '../store'; import { Color } from '../styles/Color'; diff --git a/src/store.ts b/src/store.ts index 0c9c751..3ebdf43 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,6 +1,6 @@ import create from 'zustand'; -import { Environment } from './consts/appConfig'; +import { Environment } from './consts/environments'; // Keeping everything here for now as state is simple // Will refactor into slices as necessary From edf5652bea8bb886a55334c38ceb5c637f69c50f Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 24 Sep 2022 19:08:25 -0400 Subject: [PATCH 07/11] Copy debug script from monorepo Update sdk version Minor style tweaks --- package.json | 2 +- src/components/layout/ContentFrame.tsx | 2 +- src/components/nav/Header.tsx | 2 +- src/features/debugger/debugMessage.ts | 118 +++++++++++++++++++++++++ yarn.lock | 118 ++++++++++++------------- 5 files changed, 180 insertions(+), 62 deletions(-) create mode 100644 src/features/debugger/debugMessage.ts diff --git a/package.json b/package.json index dee0c10..a6b72b8 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.1.0", "author": "J M Rossy", "dependencies": { - "@abacus-network/sdk": "^0.4.1", + "@hyperlane-xyz/sdk": "^0.5.0-beta0", "@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6", "@rainbow-me/rainbowkit": "^0.4.5", "buffer": "^6.0.3", diff --git a/src/components/layout/ContentFrame.tsx b/src/components/layout/ContentFrame.tsx index e05af34..733a46b 100644 --- a/src/components/layout/ContentFrame.tsx +++ b/src/components/layout/ContentFrame.tsx @@ -7,7 +7,7 @@ export function ContentFrame(props: PropsWithChildren) {
-
{props.children}
+
{props.children}
); diff --git a/src/components/nav/Header.tsx b/src/components/nav/Header.tsx index 470c9d7..5ffb283 100644 --- a/src/components/nav/Header.tsx +++ b/src/components/nav/Header.tsx @@ -108,7 +108,7 @@ function NetworkSelector() {
{/* */} setEnvironment(e as Environment)} diff --git a/src/features/debugger/debugMessage.ts b/src/features/debugger/debugMessage.ts new file mode 100644 index 0000000..6a74fbc --- /dev/null +++ b/src/features/debugger/debugMessage.ts @@ -0,0 +1,118 @@ +// Based on debug script in monorepo +// https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/infra/scripts/debug-message.ts +import { IMessageRecipient__factory } from '@hyperlane-xyz/core'; +import { + ChainName, + Chains, + DispatchedMessage, + DomainIdToChainName, + HyperlaneCore, + MultiProvider, + chainConnectionConfigs, +} from '@hyperlane-xyz/sdk'; +import { utils } from '@hyperlane-xyz/utils'; + +import { Environment } from '../../consts/environments'; + +export async function debugMessageForHash(txHash: string, environment: Environment) { + const originChain = Chains.ethereum; // TODO check every chain + + // TODO use RPC with api keys + const multiProvider = new MultiProvider(chainConnectionConfigs); + + const core = HyperlaneCore.fromEnvironment(environment, multiProvider); + + const originProvider = multiProvider.getChainProvider(originChain); + const dispatchReceipt = await originProvider.getTransactionReceipt(txHash); + const dispatchedMessages = core.getDispatchedMessages(dispatchReceipt); + + // 1 indexed for human friendly logs + let currentMessage = 1; + for (const message of dispatchedMessages) { + console.log(`Message ${currentMessage} of ${dispatchedMessages.length}...`); + await checkMessage(core, multiProvider, message); + console.log('=========='); + currentMessage++; + } + console.log(`Evaluated ${dispatchedMessages.length} messages`); +} + +async function checkMessage( + core: HyperlaneCore, + multiProvider: MultiProvider, + message: DispatchedMessage, +) { + console.log(`Leaf index: ${message.leafIndex.toString()}`); + console.log(`Raw bytes: ${message.message}`); + console.log('Parsed message:', message.parsed); + + const destinationChain = DomainIdToChainName[message.parsed.destination]; + + if (destinationChain === undefined) { + console.error(`ERROR: Unknown destination domain ${message.parsed.destination}`); + return; + } + + console.log(`Destination chain: ${destinationChain}`); + + if (!core.knownChain(destinationChain)) { + console.error(`ERROR: destination chain ${destinationChain} unknown for environment`); + return; + } + + const destinationInbox = core.getMailboxPair( + DomainIdToChainName[message.parsed.origin], + destinationChain, + ).destinationInbox; + + const messageHash = utils.messageHash(message.message, message.leafIndex); + console.log(`Message hash: ${messageHash}`); + + const processed = await destinationInbox.messages(messageHash); + if (processed === 1) { + console.log('Message has already been processed'); + + // TODO: look for past events to find the exact tx in which the message was processed. + + return; + } else { + console.log('Message not yet processed'); + } + + const recipientAddress = utils.bytes32ToAddress(message.parsed.recipient); + const recipientIsContract = await isContract(multiProvider, destinationChain, recipientAddress); + + if (!recipientIsContract) { + console.error( + `ERROR: recipient address ${recipientAddress} is not a contract, maybe a malformed bytes32 recipient?`, + ); + return; + } + + const destinationProvider = multiProvider.getChainProvider(destinationChain); + const recipient = IMessageRecipient__factory.connect(recipientAddress, destinationProvider); + + try { + await recipient.estimateGas.handle( + message.parsed.origin, + message.parsed.sender, + message.parsed.body, + { from: destinationInbox.address }, + ); + console.log('Calling recipient `handle` function from the inbox does not revert'); + } catch (err: any) { + console.error(`Error calling recipient \`handle\` function from the inbox`); + if (err.reason) { + console.error('Reason: ', err.reason); + } else { + console.error(err); + } + } +} + +async function isContract(multiProvider: MultiProvider, chain: ChainName, address: string) { + const provider = multiProvider.getChainProvider(chain); + const code = await provider.getCode(address); + // "Empty" code + return code && code !== '0x'; +} diff --git a/yarn.lock b/yarn.lock index 449037d..8043789 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,64 +5,6 @@ __metadata: version: 6 cacheKey: 8 -"@abacus-network/app@npm:0.4.1": - version: 0.4.1 - resolution: "@abacus-network/app@npm:0.4.1" - dependencies: - "@abacus-network/core": 0.4.1 - "@abacus-network/utils": 0.4.1 - "@openzeppelin/contracts-upgradeable": ^4.5.0 - checksum: bf1dc46b769bfef8ef2f1b568b5ea23f5c4148e68e3771d95510adb919b1ea030cff9080f9fc8a05889f973b1ff718b783414d8e5e86749e78f56799c38fe4df - languageName: node - linkType: hard - -"@abacus-network/celo-ethers-provider@npm:^0.1.0": - version: 0.1.0 - resolution: "@abacus-network/celo-ethers-provider@npm:0.1.0" - peerDependencies: - ethers: ^5 - checksum: 06f440366bbd9ddf9962aa46c471be1fb587909c4da71a0d816497a5c0c8597e9b315d70fe9ae728ed51fc936097da50da2cd18f750861f3d602660b8ea1ecfd - languageName: node - linkType: hard - -"@abacus-network/core@npm:0.4.1": - version: 0.4.1 - resolution: "@abacus-network/core@npm:0.4.1" - dependencies: - "@abacus-network/utils": 0.4.1 - "@openzeppelin/contracts": ^4.6.0 - "@openzeppelin/contracts-upgradeable": ^4.6.0 - "@summa-tx/memview-sol": ^2.0.0 - checksum: 5fdeb32eb33ad6a470b4b7f801ed419da2065cb6c6f155d651576cd66379663e3d1b35f9b9016d581b1ffd7aa038d7cdab8f8574bc259d801bc317753de41e1d - languageName: node - linkType: hard - -"@abacus-network/sdk@npm:^0.4.1": - version: 0.4.1 - resolution: "@abacus-network/sdk@npm:0.4.1" - dependencies: - "@abacus-network/app": 0.4.1 - "@abacus-network/celo-ethers-provider": ^0.1.0 - "@abacus-network/core": 0.4.1 - "@abacus-network/utils": 0.4.1 - "@types/debug": ^4.1.7 - coingecko-api: ^1.0.10 - cross-fetch: ^3.1.5 - debug: ^4.3.4 - ethers: ^5.6.8 - checksum: d6f43d26666b07711ab82688b4b5d5b3de718019223b048b753dbf04ca38eed79bc3f634c9de66592babb86b1cfcf3250dc7065e3ae8bb3386a98ca7aed7258c - languageName: node - linkType: hard - -"@abacus-network/utils@npm:0.4.1": - version: 0.4.1 - resolution: "@abacus-network/utils@npm:0.4.1" - dependencies: - ethers: ^5.6.8 - checksum: dd69c911619412b138e12a1bfa1c52a57e56b40cdd416122cb98a4c04032295978b45cfecc47b8038af72d01eab9811245cfb6e3d50cd21ec11b263f426e6b1a - languageName: node - linkType: hard - "@babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.18.6": version: 7.18.6 resolution: "@babel/code-frame@npm:7.18.6" @@ -904,11 +846,43 @@ __metadata: languageName: node linkType: hard +"@hyperlane-xyz/app@npm:0.5.0-beta0": + version: 0.5.0-beta0 + resolution: "@hyperlane-xyz/app@npm:0.5.0-beta0" + dependencies: + "@hyperlane-xyz/core": 0.5.0-beta0 + "@hyperlane-xyz/utils": 0.5.0-beta0 + "@openzeppelin/contracts-upgradeable": ^4.5.0 + checksum: 14dac843eb418bd9807116bb0dd56aca08bd4a8645cd635f534dc75bb8fbfd1b5a12c1d1b2e9343d12592798e827f191c48725d0e6d943e2e9d7fe3bd3fddf47 + languageName: node + linkType: hard + +"@hyperlane-xyz/celo-ethers-provider@npm:^0.1.1": + version: 0.1.1 + resolution: "@hyperlane-xyz/celo-ethers-provider@npm:0.1.1" + peerDependencies: + ethers: ^5 + checksum: 5ea495505b3e4338ec6c419f69b66af2d35b86e7cdab9bc7ee65ba4a233729b2143b0a72cc637f8e8795ecacc18eeda5ac803e8b3de9a63e7ceb5dc14014b3d4 + languageName: node + linkType: hard + +"@hyperlane-xyz/core@npm:0.5.0-beta0": + version: 0.5.0-beta0 + resolution: "@hyperlane-xyz/core@npm:0.5.0-beta0" + dependencies: + "@hyperlane-xyz/utils": 0.5.0-beta0 + "@openzeppelin/contracts": ^4.6.0 + "@openzeppelin/contracts-upgradeable": ^4.6.0 + "@summa-tx/memview-sol": ^2.0.0 + checksum: 7c3f88d4140d63b50013feddd1a13275e28eeb1ff1e7cd119480aee904a11c6ff0b50535159e90058b6a80cf28176a7ef0bdd13a20e99f82ef722a34b807a193 + languageName: node + linkType: hard + "@hyperlane-xyz/explorer@workspace:.": version: 0.0.0-use.local resolution: "@hyperlane-xyz/explorer@workspace:." dependencies: - "@abacus-network/sdk": ^0.4.1 + "@hyperlane-xyz/sdk": ^0.5.0-beta0 "@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6" "@rainbow-me/rainbowkit": ^0.4.5 "@trivago/prettier-plugin-sort-imports": ^3.2.0 @@ -941,6 +915,32 @@ __metadata: languageName: unknown linkType: soft +"@hyperlane-xyz/sdk@npm:^0.5.0-beta0": + version: 0.5.0-beta0 + resolution: "@hyperlane-xyz/sdk@npm:0.5.0-beta0" + dependencies: + "@hyperlane-xyz/app": 0.5.0-beta0 + "@hyperlane-xyz/celo-ethers-provider": ^0.1.1 + "@hyperlane-xyz/core": 0.5.0-beta0 + "@hyperlane-xyz/utils": 0.5.0-beta0 + "@types/debug": ^4.1.7 + coingecko-api: ^1.0.10 + cross-fetch: ^3.1.5 + debug: ^4.3.4 + ethers: ^5.6.8 + checksum: 85ef5c70ad290f1ff67297fdd76419d8b4290b22c3a1eb9ed5c1ac4876c7fef5c11471a063190aa8940156bc622e6527950d24aac9e58897cd301da33c41c1c7 + languageName: node + linkType: hard + +"@hyperlane-xyz/utils@npm:0.5.0-beta0": + version: 0.5.0-beta0 + resolution: "@hyperlane-xyz/utils@npm:0.5.0-beta0" + dependencies: + ethers: ^5.6.8 + checksum: 7628b0624a1b7bbab77b04ada38d0842dee8076273f58c0cd608877b430e46f2d965de0293d53b6bdcfe66b4fb36840ded0ee2a9e30894394415be38817e223f + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.3.2": version: 0.3.2 resolution: "@jridgewell/gen-mapping@npm:0.3.2" From e95f5cf6c5ccc7384dd6f2d85d97f380add17e91 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sat, 24 Sep 2022 19:47:22 -0400 Subject: [PATCH 08/11] Setup react-query --- .prettierrc | 2 +- package.json | 1 + src/components/search/SearchError.tsx | 41 +++++++++++++++++++++++---- src/consts/appConfig.ts | 2 +- src/consts/networksConfig.ts | 2 +- src/features/debugger/TxDebugger.tsx | 30 +++++++++++++------- src/features/search/MessageSearch.tsx | 3 +- src/pages/_app.tsx | 15 ++++++---- yarn.lock | 27 ++++++++++++++++++ 9 files changed, 98 insertions(+), 25 deletions(-) diff --git a/.prettierrc b/.prettierrc index 8065834..992ca1e 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,7 +3,7 @@ "printWidth": 100, "singleQuote": true, "trailingComma": "all", - "importOrder": ["^@abacus-network/(.*)$", "^../(.*)$", "^./(.*)$"], + "importOrder": ["^@hyperlane-xyz/(.*)$", "^../(.*)$", "^./(.*)$"], "importOrderSeparation": true, "importOrderSortSpecifiers": true } diff --git a/package.json b/package.json index a6b72b8..d68a5db 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@hyperlane-xyz/sdk": "^0.5.0-beta0", "@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6", "@rainbow-me/rainbowkit": "^0.4.5", + "@tanstack/react-query": "^4.6.0", "buffer": "^6.0.3", "ethers": "^5.6.8", "formik": "^2.2.9", diff --git a/src/components/search/SearchError.tsx b/src/components/search/SearchError.tsx index 2a4f756..5c8fb94 100644 --- a/src/components/search/SearchError.tsx +++ b/src/components/search/SearchError.tsx @@ -1,5 +1,6 @@ import Image from 'next/future/image'; +import BugIcon from '../../images/icons/bug.svg'; import ErrorIcon from '../../images/icons/error-circle.svg'; import SearchOffIcon from '../../images/icons/search-off.svg'; import ShrugIcon from '../../images/icons/shrug.svg'; @@ -31,25 +32,55 @@ export function SearchError({ ); } -export function SearchInvalidError({ show }: { show: boolean }) { +export function NoSearchError({ show }: { show: boolean }) { + return ( + + ); +} + +export function SearchInvalidError({ + show, + allowAddress, +}: { + show: boolean; + allowAddress: boolean; +}) { return ( ); } -export function SearchEmptyError({ show, hasInput }: { show: boolean; hasInput: boolean }) { +export function SearchEmptyError({ + show, + hasInput, + allowAddress, +}: { + show: boolean; + hasInput: boolean; + allowAddress: boolean; +}) { return ( diff --git a/src/consts/appConfig.ts b/src/consts/appConfig.ts index 170c6b2..4e36d9c 100644 --- a/src/consts/appConfig.ts +++ b/src/consts/appConfig.ts @@ -9,7 +9,7 @@ export const configs: Record = { debug: isDevMode, version, url: 'https://explorer.hyperlane.xyz', - apiUrl: 'https://abacus-explorer-api.hasura.app/v1/graphql', + apiUrl: 'https://abacus-explorer-api.hasura.app/v1/graphql', // TODO change }, testnet2: { environment: Environment.Testnet2, diff --git a/src/consts/networksConfig.ts b/src/consts/networksConfig.ts index 559ef86..d05e899 100644 --- a/src/consts/networksConfig.ts +++ b/src/consts/networksConfig.ts @@ -1,6 +1,6 @@ import { Chain, allChains as allChainsWagmi, chain } from 'wagmi'; -import { chainConnectionConfigs } from '@abacus-network/sdk'; +import { chainConnectionConfigs } from '@hyperlane-xyz/sdk'; export const testConfigs = { goerli: chainConnectionConfigs.goerli, diff --git a/src/features/debugger/TxDebugger.tsx b/src/features/debugger/TxDebugger.tsx index f4b37d7..32a133a 100644 --- a/src/features/debugger/TxDebugger.tsx +++ b/src/features/debugger/TxDebugger.tsx @@ -1,8 +1,10 @@ -import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { useCallback, useState } from 'react'; import { Fade } from '../../components/animation/Fade'; import { SearchBar } from '../../components/search/SearchBar'; import { + NoSearchError, SearchEmptyError, SearchInvalidError, SearchUnknownError, @@ -13,6 +15,8 @@ import useDebounce from '../../utils/debounce'; import { sanitizeString } from '../../utils/string'; import { isValidSearchQuery } from '../search/utils'; +import { debugMessageForHash } from './debugMessage'; + export function TxDebugger() { const environment = useStore((s) => s.environment); @@ -21,11 +25,15 @@ export function TxDebugger() { const debouncedSearchInput = useDebounce(searchInput, 750); const hasInput = !!debouncedSearchInput; const sanitizedInput = sanitizeString(debouncedSearchInput); - const isValidInput = hasInput ? isValidSearchQuery(sanitizedInput, false) : true; + const isValidInput = isValidSearchQuery(sanitizedInput, false); - const fetching = false; - const hasError = false; - const txResult = {}; + // Debugger query + const query = useCallback(() => { + if (!isValidInput || !sanitizedInput) return null; + else return debugMessageForHash(sanitizedInput, environment); + }, [isValidInput, sanitizedInput, environment]); + const { isLoading: fetching, error, data } = useQuery(['debugMessage'], query); + const hasError = !!error; return ( <> @@ -36,19 +44,19 @@ export function TxDebugger() { placeholder="Search transaction hash to debug message" />
- {/* Content header and filter bar */}

{`Transaction Debugger (${envDisplayValue[environment]})`}

- {/* Message list */} - {JSON.stringify(txResult)} - - + {JSON.stringify(data)} + + +
); diff --git a/src/features/search/MessageSearch.tsx b/src/features/search/MessageSearch.tsx index 520ec52..54a81a1 100644 --- a/src/features/search/MessageSearch.tsx +++ b/src/features/search/MessageSearch.tsx @@ -125,11 +125,12 @@ export function MessageSearch() { ))} - +
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 5227f31..b801128 100755 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -5,6 +5,7 @@ import { wallet, } from '@rainbow-me/rainbowkit'; import '@rainbow-me/rainbowkit/styles.css'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { AppProps } from 'next/app'; import { ToastContainer, Zoom, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; @@ -52,6 +53,8 @@ const urqlClients: Record = { }), }; +const reactQueryClient = new QueryClient(); + export default function App({ Component, router, pageProps }: AppProps) { const environment = useStore((s) => s.environment); @@ -74,11 +77,13 @@ export default function App({ Component, router, pageProps }: AppProps) { fontStack: 'system', })} > - - - - - + + + + + + + diff --git a/yarn.lock b/yarn.lock index 8043789..d33dba4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -885,6 +885,7 @@ __metadata: "@hyperlane-xyz/sdk": ^0.5.0-beta0 "@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6" "@rainbow-me/rainbowkit": ^0.4.5 + "@tanstack/react-query": ^4.6.0 "@trivago/prettier-plugin-sort-imports": ^3.2.0 "@types/node": 18.0.4 "@types/react": 18.0.15 @@ -1257,6 +1258,32 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-core@npm:4.6.0": + version: 4.6.0 + resolution: "@tanstack/query-core@npm:4.6.0" + checksum: 945a3b1ddc89ddf484f828cee0be5db0939f51865b2518f8c4ef351c6a3fc992f8c9fbbd32a5bf16b97d0250e95f43c1e571d5a1dfefd99ab835ffe0f934b159 + languageName: node + linkType: hard + +"@tanstack/react-query@npm:^4.6.0": + version: 4.6.0 + resolution: "@tanstack/react-query@npm:4.6.0" + dependencies: + "@tanstack/query-core": 4.6.0 + use-sync-external-store: ^1.2.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-native: "*" + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + checksum: 39b9f5a71b1c699927db515419889b04e60410472481c5b08645eda8310c73c0c8acfcdffd9d63a5730eefff5e74a4a84939082fbe467d3af0571d837978fbb5 + languageName: node + linkType: hard + "@tootallnate/once@npm:2": version: 2.0.0 resolution: "@tootallnate/once@npm:2.0.0" From ab4a60c1d8ae8abd8e41845c1c10ac9b3ae6e2e1 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 25 Sep 2022 13:26:31 -0400 Subject: [PATCH 09/11] Rework debugMessage for use in explorer --- src/features/debugger/TxDebugger.tsx | 21 ++- src/features/debugger/debugMessage.ts | 208 +++++++++++++++++++++----- src/utils/errors.ts | 10 ++ src/utils/string.ts | 13 ++ 4 files changed, 203 insertions(+), 49 deletions(-) create mode 100644 src/utils/errors.ts diff --git a/src/features/debugger/TxDebugger.tsx b/src/features/debugger/TxDebugger.tsx index 32a133a..444f38d 100644 --- a/src/features/debugger/TxDebugger.tsx +++ b/src/features/debugger/TxDebugger.tsx @@ -1,5 +1,5 @@ import { useQuery } from '@tanstack/react-query'; -import { useCallback, useState } from 'react'; +import { useState } from 'react'; import { Fade } from '../../components/animation/Fade'; import { SearchBar } from '../../components/search/SearchBar'; @@ -27,13 +27,18 @@ export function TxDebugger() { const sanitizedInput = sanitizeString(debouncedSearchInput); const isValidInput = isValidSearchQuery(sanitizedInput, false); - // Debugger query - const query = useCallback(() => { - if (!isValidInput || !sanitizedInput) return null; - else return debugMessageForHash(sanitizedInput, environment); - }, [isValidInput, sanitizedInput, environment]); - const { isLoading: fetching, error, data } = useQuery(['debugMessage'], query); - const hasError = !!error; + const { + isLoading: fetching, + isError: hasError, + data, + } = useQuery( + ['debugMessage', isValidInput, sanitizedInput, environment], + () => { + if (!isValidInput || !sanitizedInput) return null; + else return debugMessageForHash(sanitizedInput, environment); + }, + { retry: false }, + ); return ( <> diff --git a/src/features/debugger/debugMessage.ts b/src/features/debugger/debugMessage.ts index 6a74fbc..14b752e 100644 --- a/src/features/debugger/debugMessage.ts +++ b/src/features/debugger/debugMessage.ts @@ -3,7 +3,6 @@ import { IMessageRecipient__factory } from '@hyperlane-xyz/core'; import { ChainName, - Chains, DispatchedMessage, DomainIdToChainName, HyperlaneCore, @@ -13,28 +12,129 @@ import { import { utils } from '@hyperlane-xyz/utils'; import { Environment } from '../../consts/environments'; +import { errorToString } from '../../utils/errors'; +import { logger } from '../../utils/logger'; +import { chunk } from '../../utils/string'; + +export enum TxDebugStatus { + NotFound = 'notFound', + NoMessages = 'noMessages', + MessagesFound = 'messagesFound', +} + +export enum MessageDebugStatus { + NoErrorsFound = 'noErrorsFound', + InvalidDestDomain = 'invalidDestDomain', + UnknownDestChain = 'unknownDestChain', + RecipientNotContract = 'RecipientNotContract', + HandleCallFailure = 'handleCallFailure', +} + +export interface DebugNotFoundResult { + status: TxDebugStatus.NotFound; + details: string; +} + +export interface DebugNoMessagesResult { + status: TxDebugStatus.NoMessages; + chainName: string; + details: string; + explorerLink?: string; +} -export async function debugMessageForHash(txHash: string, environment: Environment) { - const originChain = Chains.ethereum; // TODO check every chain +interface MessageDetails { + status: MessageDebugStatus; + properties: Map; + summary: string; +} + +export interface DebugMessagesFoundResult { + status: TxDebugStatus.MessagesFound; + chainName: string; + explorerLink?: string; + messageDetails: MessageDetails[]; +} + +type MessageDebugResult = DebugNotFoundResult | DebugNoMessagesResult | DebugMessagesFoundResult; +export async function debugMessageForHash( + txHash: string, + environment: Environment, +): Promise { // TODO use RPC with api keys const multiProvider = new MultiProvider(chainConnectionConfigs); + const txDetails = await findTransactionDetails(txHash, multiProvider); + if (!txDetails?.transactionReceipt) { + return { + status: TxDebugStatus.NotFound, + details: 'No transaction found for this hash on any supported networks.', + }; + } + + const { transactionReceipt, chainName, explorerLink } = txDetails; const core = HyperlaneCore.fromEnvironment(environment, multiProvider); + const dispatchedMessages = core.getDispatchedMessages(transactionReceipt); + + if (!dispatchedMessages?.length) { + return { + status: TxDebugStatus.NoMessages, + details: + 'No messages found for this transaction. Please check that the hash and environment are set correctly.', + chainName, + explorerLink, + }; + } - const originProvider = multiProvider.getChainProvider(originChain); - const dispatchReceipt = await originProvider.getTransactionReceipt(txHash); - const dispatchedMessages = core.getDispatchedMessages(dispatchReceipt); - - // 1 indexed for human friendly logs - let currentMessage = 1; - for (const message of dispatchedMessages) { - console.log(`Message ${currentMessage} of ${dispatchedMessages.length}...`); - await checkMessage(core, multiProvider, message); - console.log('=========='); - currentMessage++; + logger.debug(`Found ${dispatchedMessages.length} messages`); + const messageDetails: MessageDetails[] = []; + for (let i = 0; i < dispatchedMessages.length; i++) { + logger.debug(`Checking message ${i} of ${dispatchedMessages.length}`); + messageDetails.push(await checkMessage(core, multiProvider, dispatchedMessages[i])); + logger.debug(`Done checking message ${i}`); + } + return { + status: TxDebugStatus.MessagesFound, + chainName, + explorerLink, + messageDetails, + }; +} + +async function findTransactionDetails(txHash: string, multiProvider: MultiProvider) { + const chains = multiProvider.chains().filter((n) => !n.startsWith('test')); + const chainChunks = chunk(chains, 10); + for (const chunk of chainChunks) { + try { + const queries = chunk.map((c) => fetchTransactionDetails(txHash, multiProvider, c)); + const result = await Promise.any(queries); + return result; + } catch (error) { + logger.debug('Tx not found, trying next chunk'); + } + } + logger.debug('Tx not found on any networks'); + return null; +} + +async function fetchTransactionDetails( + txHash: string, + multiProvider: MultiProvider, + chainName: ChainName, +) { + const { provider, blockExplorerUrl } = multiProvider.getChainConnection(chainName); + // TODO explorer may be faster, more robust way to get tx and its logs + // Note: receipt is null if tx not found + const transactionReceipt = await provider.getTransactionReceipt(txHash); + if (transactionReceipt) { + logger.info('Tx found', txHash, chainName); + // TODO use getTxExplorerLink here, must reconcile wagmi consts and sdk consts + const explorerLink = blockExplorerUrl ? `${blockExplorerUrl}/tx/${txHash}` : undefined; + return { transactionReceipt, chainName, explorerLink }; + } else { + logger.debug('Tx not found', txHash, chainName); + throw new Error(`Tx not found on ${chainName}`); } - console.log(`Evaluated ${dispatchedMessages.length} messages`); } async function checkMessage( @@ -42,22 +142,36 @@ async function checkMessage( multiProvider: MultiProvider, message: DispatchedMessage, ) { - console.log(`Leaf index: ${message.leafIndex.toString()}`); - console.log(`Raw bytes: ${message.message}`); - console.log('Parsed message:', message.parsed); + logger.debug(JSON.stringify(message)); + const properties = new Map(); + properties.set('Sender', message.parsed.sender.toString()); + properties.set('Recipient', message.parsed.sender.toString()); + properties.set('Origin Domain', message.parsed.origin.toString()); + properties.set('Destination Domain', message.parsed.destination.toString()); + properties.set('Leaf index', message.leafIndex.toString()); + properties.set('Raw Bytes', message.message); const destinationChain = DomainIdToChainName[message.parsed.destination]; - if (destinationChain === undefined) { - console.error(`ERROR: Unknown destination domain ${message.parsed.destination}`); - return; + if (!destinationChain) { + logger.info(`Unknown destination domain ${message.parsed.destination}`); + return { + status: MessageDebugStatus.InvalidDestDomain, + properties, + summary: + 'The destination domain id is invalid. Note, domain ids usually do not match chain ids. See https://docs.hyperlane.xyz/hyperlane-docs/developers/domains', + }; } - console.log(`Destination chain: ${destinationChain}`); + logger.debug(`Destination chain: ${destinationChain}`); if (!core.knownChain(destinationChain)) { - console.error(`ERROR: destination chain ${destinationChain} unknown for environment`); - return; + logger.info(`Destination chain ${destinationChain} unknown for environment`); + return { + status: MessageDebugStatus.UnknownDestChain, + properties, + summary: `Destination chain ${destinationChain} is not included in this message's environment. See https://docs.hyperlane.xyz/hyperlane-docs/developers/domains`, + }; } const destinationInbox = core.getMailboxPair( @@ -66,27 +180,31 @@ async function checkMessage( ).destinationInbox; const messageHash = utils.messageHash(message.message, message.leafIndex); - console.log(`Message hash: ${messageHash}`); + logger.debug(`Message hash: ${messageHash}`); const processed = await destinationInbox.messages(messageHash); if (processed === 1) { - console.log('Message has already been processed'); - + logger.info('Message has already been processed'); // TODO: look for past events to find the exact tx in which the message was processed. - - return; + return { + status: MessageDebugStatus.NoErrorsFound, + properties, + summary: 'No errors found, this message has already been processed.', + }; } else { - console.log('Message not yet processed'); + logger.debug('Message not yet processed'); } const recipientAddress = utils.bytes32ToAddress(message.parsed.recipient); const recipientIsContract = await isContract(multiProvider, destinationChain, recipientAddress); if (!recipientIsContract) { - console.error( - `ERROR: recipient address ${recipientAddress} is not a contract, maybe a malformed bytes32 recipient?`, - ); - return; + logger.info(`Recipient address ${recipientAddress} is not a contract`); + return { + status: MessageDebugStatus.RecipientNotContract, + properties, + summary: `Recipient address ${recipientAddress} is not a contract. Ensure bytes32 value is not malformed.`, + }; } const destinationProvider = multiProvider.getChainProvider(destinationChain); @@ -99,14 +217,22 @@ async function checkMessage( message.parsed.body, { from: destinationInbox.address }, ); - console.log('Calling recipient `handle` function from the inbox does not revert'); + logger.debug('Calling recipient `handle` function from the inbox does not revert'); + return { + status: MessageDebugStatus.NoErrorsFound, + properties, + summary: 'No errors found, this message appears to be deliverable.', + }; } catch (err: any) { - console.error(`Error calling recipient \`handle\` function from the inbox`); - if (err.reason) { - console.error('Reason: ', err.reason); - } else { - console.error(err); - } + logger.info(`Error calling recipient handle function from the inbox`); + const errorString = errorToString(err); + logger.debug(errorString); + return { + status: MessageDebugStatus.HandleCallFailure, + properties, + // TODO format the error string better to be easier to understand + summary: `Error calling handle on the recipient contract. Details: ${errorString}`, + }; } } diff --git a/src/utils/errors.ts b/src/utils/errors.ts new file mode 100644 index 0000000..447dd4a --- /dev/null +++ b/src/utils/errors.ts @@ -0,0 +1,10 @@ +import { trimToLength } from './string'; + +export function errorToString(error: any, maxLength = 300) { + if (!error) return 'Unknown Error'; + if (typeof error === 'string') return trimToLength(error, maxLength); + if (typeof error === 'number') return `Error code: ${error}`; + const details = error.message || error.reason || error; + if (typeof details === 'string') return trimToLength(details, maxLength); + return trimToLength(JSON.stringify(details), maxLength); +} diff --git a/src/utils/string.ts b/src/utils/string.ts index 1555a9d..9bec440 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -17,3 +17,16 @@ export function trimToLength(value: string, maxLength: number) { const trimmed = value.trim(); return trimmed.length > maxLength ? trimmed.substring(0, maxLength) + '...' : trimmed; } + +interface Sliceable { + length: number; + slice: (i: number, j: number) => any; +} + +export function chunk(str: T, size: number) { + const R: Array = []; + for (let i = 0; i < str.length; i += size) { + R.push(str.slice(i, i + size)); + } + return R; +} From f976161642083d315f956003b893536b9ecf2d96 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 25 Sep 2022 17:08:50 -0400 Subject: [PATCH 10/11] Implement components for debug message result --- src/components/nav/Header.tsx | 8 +-- src/features/debugger/TxDebugger.tsx | 83 ++++++++++++++++++++++++++- src/features/debugger/debugMessage.ts | 9 ++- src/features/search/MessageSearch.tsx | 2 +- 4 files changed, 91 insertions(+), 11 deletions(-) diff --git a/src/components/nav/Header.tsx b/src/components/nav/Header.tsx index 5ffb283..da29c06 100644 --- a/src/components/nav/Header.tsx +++ b/src/components/nav/Header.tsx @@ -32,7 +32,7 @@ export function Header({ pathName }: { pathName: string }) {
-
+
+
{/* Dropdown menu, used on mobile */} - + ); } diff --git a/src/features/debugger/TxDebugger.tsx b/src/features/debugger/TxDebugger.tsx index 444f38d..42fe68d 100644 --- a/src/features/debugger/TxDebugger.tsx +++ b/src/features/debugger/TxDebugger.tsx @@ -1,7 +1,9 @@ import { useQuery } from '@tanstack/react-query'; +import Image from 'next/future/image'; import { useState } from 'react'; import { Fade } from '../../components/animation/Fade'; +import { CopyButton } from '../../components/buttons/CopyButton'; import { SearchBar } from '../../components/search/SearchBar'; import { NoSearchError, @@ -10,12 +12,13 @@ import { SearchUnknownError, } from '../../components/search/SearchError'; import { envDisplayValue } from '../../consts/environments'; +import ShrugIcon from '../../images/icons/shrug.svg'; import { useStore } from '../../store'; import useDebounce from '../../utils/debounce'; import { sanitizeString } from '../../utils/string'; import { isValidSearchQuery } from '../search/utils'; -import { debugMessageForHash } from './debugMessage'; +import { MessageDebugResult, TxDebugStatus, debugMessageForHash } from './debugMessage'; export function TxDebugger() { const environment = useStore((s) => s.environment); @@ -49,11 +52,15 @@ export function TxDebugger() { placeholder="Search transaction hash to debug message" />
-
+

{`Transaction Debugger (${envDisplayValue[environment]})`}

- {JSON.stringify(data)} + +
+ +
+
); } + +function DebugResult({ result }: { result: MessageDebugResult | null | undefined }) { + if (!result) return null; + + if (result.status === TxDebugStatus.NotFound) { + return ( +
+ +

No transaction found

+

{result.details}

+
+ ); + } + + if (result.status === TxDebugStatus.NoMessages) { + return ( +
+ +

No message found

+

{result.details}

+ +
+ ); + } + + if (result.status === TxDebugStatus.MessagesFound) { + return ( + <> + {result.messageDetails.map((m, i) => ( +
+

{`Message ${i + 1} / ${ + result.messageDetails.length + }`}

+

{m.summary}

+
+ {Array.from(m.properties.entries()).map(([key, val]) => ( +
+ +
+ {val} +
+ {val.length > 20 && ( + + )} +
+ ))} +
+
+ ))} + + + ); + } + + return null; +} + +function TxExplorerLink({ href }: { href: string | undefined }) { + if (!href) return null; + return ( + + View transaction in explorer + + ); +} diff --git a/src/features/debugger/debugMessage.ts b/src/features/debugger/debugMessage.ts index 14b752e..1a83c2d 100644 --- a/src/features/debugger/debugMessage.ts +++ b/src/features/debugger/debugMessage.ts @@ -55,7 +55,10 @@ export interface DebugMessagesFoundResult { messageDetails: MessageDetails[]; } -type MessageDebugResult = DebugNotFoundResult | DebugNoMessagesResult | DebugMessagesFoundResult; +export type MessageDebugResult = + | DebugNotFoundResult + | DebugNoMessagesResult + | DebugMessagesFoundResult; export async function debugMessageForHash( txHash: string, @@ -89,9 +92,9 @@ export async function debugMessageForHash( logger.debug(`Found ${dispatchedMessages.length} messages`); const messageDetails: MessageDetails[] = []; for (let i = 0; i < dispatchedMessages.length; i++) { - logger.debug(`Checking message ${i} of ${dispatchedMessages.length}`); + logger.debug(`Checking message ${i + 1} of ${dispatchedMessages.length}`); messageDetails.push(await checkMessage(core, multiProvider, dispatchedMessages[i])); - logger.debug(`Done checking message ${i}`); + logger.debug(`Done checking message ${i + 1}`); } return { status: TxDebugStatus.MessagesFound, diff --git a/src/features/search/MessageSearch.tsx b/src/features/search/MessageSearch.tsx index 54a81a1..e3d7a5e 100644 --- a/src/features/search/MessageSearch.tsx +++ b/src/features/search/MessageSearch.tsx @@ -91,7 +91,7 @@ export function MessageSearch() { />
{/* Content header and filter bar */} -
+

{!hasInput ? 'Latest Messages' : 'Search Results'}

From 9dfcd81cb43d496784e5c13c8e596290de8273a7 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Mon, 26 Sep 2022 12:19:28 -0400 Subject: [PATCH 11/11] Show chain name in explorer link --- src/features/debugger/TxDebugger.tsx | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/features/debugger/TxDebugger.tsx b/src/features/debugger/TxDebugger.tsx index 42fe68d..e9fea08 100644 --- a/src/features/debugger/TxDebugger.tsx +++ b/src/features/debugger/TxDebugger.tsx @@ -15,7 +15,7 @@ import { envDisplayValue } from '../../consts/environments'; import ShrugIcon from '../../images/icons/shrug.svg'; import { useStore } from '../../store'; import useDebounce from '../../utils/debounce'; -import { sanitizeString } from '../../utils/string'; +import { sanitizeString, toTitleCase } from '../../utils/string'; import { isValidSearchQuery } from '../search/utils'; import { MessageDebugResult, TxDebugStatus, debugMessageForHash } from './debugMessage'; @@ -79,7 +79,7 @@ function DebugResult({ result }: { result: MessageDebugResult | null | undefined if (result.status === TxDebugStatus.NotFound) { return ( -
+

No transaction found

{result.details}

@@ -89,11 +89,11 @@ function DebugResult({ result }: { result: MessageDebugResult | null | undefined if (result.status === TxDebugStatus.NoMessages) { return ( -
+

No message found

{result.details}

- +
); } @@ -122,7 +122,7 @@ function DebugResult({ result }: { result: MessageDebugResult | null | undefined
))} - + ); } @@ -130,8 +130,14 @@ function DebugResult({ result }: { result: MessageDebugResult | null | undefined return null; } -function TxExplorerLink({ href }: { href: string | undefined }) { - if (!href) return null; +function TxExplorerLink({ + href, + chainName, +}: { + href: string | undefined; + chainName: string | undefined; +}) { + if (!href || !chainName) return null; return ( - View transaction in explorer + {`View transaction in ${toTitleCase(chainName)} explorer`} ); }