Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@ const nextConfig = {

reactStrictMode: true,

// Transpile hyperlane packages to apply webpack aliases
transpilePackages: ['@hyperlane-xyz/utils', '@hyperlane-xyz/widgets'],

// Configure webpack to mock pino during SSR to avoid pino-pretty transport issues
webpack: (config, { isServer }) => {
if (isServer) {
Expand All @@ -64,15 +61,6 @@ const nextConfig = {
}
return config;
},

experimental: {
optimizePackageImports: [
'@hyperlane-xyz/registry',
'@hyperlane-xyz/sdk',
'@hyperlane-xyz/utils',
'@hyperlane-xyz/widgets',
],
},
};

module.exports = nextConfig;
14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@
"@ethersproject/transactions": "^5.7.0",
"@ethersproject/web": "^5.7.0",
"@headlessui/react": "^2.2.0",
"@hyperlane-xyz/core": "10.0.5",
"@hyperlane-xyz/core": "10.1.4-beta.c241647e30c31aadb5a07f853a8d7bbe79e10e0f",
"@hyperlane-xyz/registry": "23.9.0",
"@hyperlane-xyz/sdk": "20.1.0",
"@hyperlane-xyz/utils": "20.1.0",
"@hyperlane-xyz/widgets": "20.1.0",
"@hyperlane-xyz/relayer": "0.1.0-beta.c241647e30c31aadb5a07f853a8d7bbe79e10e0f",
"@hyperlane-xyz/sdk": "20.2.0-beta.c241647e30c31aadb5a07f853a8d7bbe79e10e0f",
"@hyperlane-xyz/utils": "20.2.0-beta.c241647e30c31aadb5a07f853a8d7bbe79e10e0f",
"@hyperlane-xyz/widgets": "20.2.0-beta.c241647e30c31aadb5a07f853a8d7bbe79e10e0f",
"@rainbow-me/rainbowkit": "^2.2.0",
"@tanstack/query-core": "^5.62.3",
"@tanstack/react-query": "^5.62.3",
"@vercel/og": "^0.8.5",
"@wagmi/connectors": "5.5.0",
"@wagmi/core": "2.16.0",
"bignumber.js": "^9.1.2",
"buffer": "^6.0.3",
"clsx": "^2.1.1",
Expand All @@ -37,6 +41,8 @@
"react-toastify": "^10.0.6",
"react-tooltip": "^5.26.3",
"urql": "^3.0.3",
"viem": "^2.21.41",
"wagmi": "^2.12.26",
"yaml": "^2.4.5",
"zod": "^3.21.2",
"zustand": "^4.5.5"
Expand Down
4,081 changes: 687 additions & 3,394 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/components/nav/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { PropsWithChildren } from 'react';
import { DropdownMenu, WideChevronIcon } from '@hyperlane-xyz/widgets';

import { docLinks, links } from '../../consts/links';
import { ConnectWalletButton } from '../../features/wallet';
import Explorer from '../../images/logos/hyperlane-explorer.svg';
import Logo from '../../images/logos/hyperlane-logo.svg';
import Name from '../../images/logos/hyperlane-name.svg';
Expand Down Expand Up @@ -66,6 +67,7 @@ export function Header({ pathName }: { pathName: string }) {
Docs
</a>
{showSearch && <MiniSearchBar />}
<ConnectWalletButton />
</nav>
{/* Dropdown menu, used on mobile */}
<div className="item-center relative mr-2 flex sm:hidden">
Expand Down
3 changes: 3 additions & 0 deletions src/consts/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const version = process?.env?.NEXT_PUBLIC_VERSION ?? null;
const registryUrl = process?.env?.NEXT_PUBLIC_REGISTRY_URL || undefined;
const registryBranch = process?.env?.NEXT_PUBLIC_REGISTRY_BRANCH || 'main';
const explorerApiKeys = JSON.parse(process?.env?.EXPLORER_API_KEYS || '{}');
const walletConnectProjectId = process?.env?.NEXT_PUBLIC_WALLET_CONNECT_ID || '';

interface Config {
debug: boolean;
Expand All @@ -12,6 +13,7 @@ interface Config {
githubProxy?: string;
registryUrl: string | undefined; // Optional URL to use a custom registry instead of the published canonical version
registryBranch?: string | undefined; // Optional customization of the registry branch instead of main
walletConnectProjectId: string; // WalletConnect project ID for wallet connections
}

export const config: Config = Object.freeze({
Expand All @@ -22,6 +24,7 @@ export const config: Config = Object.freeze({
githubProxy: 'https://proxy.hyperlane.xyz',
registryBranch,
registryUrl,
walletConnectProjectId,
});

// Based on https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/infra/config/environments/mainnet3/agent.ts
Expand Down
31 changes: 28 additions & 3 deletions src/features/messages/cards/TransactionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { MultiProtocolProvider } from '@hyperlane-xyz/sdk';
import { isAddress, isZeroish } from '@hyperlane-xyz/utils';
import { Modal, SpinnerIcon, Tooltip, useModal } from '@hyperlane-xyz/widgets';
import BigNumber from 'bignumber.js';
import dynamic from 'next/dynamic';
import { PropsWithChildren, ReactNode, useState } from 'react';
import { ChainLogo } from '../../../components/icons/ChainLogo';
import { Card } from '../../../components/layout/Card';
Expand All @@ -11,7 +12,7 @@ import { Message, MessageStatus, MessageTx, WarpRouteDetails } from '../../../ty
import { formatAddress, formatTxHash } from '../../../utils/addresses';
import { getDateTimeString, getHumanReadableTimeString } from '../../../utils/time';
import { ChainSearchModal } from '../../chains/ChainSearchModal';
import { getChainDisplayName } from '../../chains/utils';
import { getChainDisplayName, isEvmChain } from '../../chains/utils';
import { debugStatusToDesc } from '../../debugger/strings';
import { MessageDebugResult } from '../../debugger/types';
import { CollateralStatus } from '../collateral/types';
Expand All @@ -20,6 +21,12 @@ import { LabelAndCodeBlock } from './CodeBlock';
import { ActiveRebalanceModal, InsufficientCollateralWarning } from './CollateralCards';
import { KeyValueRow } from './KeyValueRow';

// Dynamic import directly from file (not barrel) to avoid bundling the full relayer dependency tree
const SelfRelayButton = dynamic(
() => import('../../relay/SelfRelayButton').then((mod) => mod.SelfRelayButton),
{ ssr: false },
);

export function OriginTransactionCard({
chainName,
domainId,
Expand Down Expand Up @@ -105,6 +112,11 @@ export function DestinationTransactionCard({
} else if (status === MessageStatus.Failing) {
// Check if this is a collateral-related failure
const hasCollateralWarning = collateralInfo.status === CollateralStatus.Insufficient;
const canSelfRelay =
message &&
isEvmChain(multiProvider, message.originDomainId) &&
isEvmChain(multiProvider, message.destinationDomainId);

content = (
<>
{hasCollateralWarning && (
Expand All @@ -124,6 +136,11 @@ export function DestinationTransactionCard({
</div>
)}
<CallDataModal debugResult={debugResult} />
{canSelfRelay && message && (
<div className="flex justify-center">
<SelfRelayButton message={message} />
</div>
)}
</DeliveryStatus>
)}
</>
Expand Down Expand Up @@ -152,9 +169,12 @@ export function DestinationTransactionCard({
</>
);
} else if (status === MessageStatus.Pending) {
// Show collateral warning for all pending messages (not just EVM)
// since Token.getBalance() now supports cross-VM collateral checking
const hasCollateralWarning = collateralInfo.status === CollateralStatus.Insufficient;
const canSelfRelay =
message &&
isEvmChain(multiProvider, message.originDomainId) &&
isEvmChain(multiProvider, message.destinationDomainId);

content = (
<>
{hasCollateralWarning && (
Expand All @@ -176,6 +196,11 @@ export function DestinationTransactionCard({
<SpinnerIcon width={40} height={40} />
</div>
<CallDataModal debugResult={debugResult} />
{canSelfRelay && message && (
<div className="flex justify-center">
<SelfRelayButton message={message} />
</div>
)}
</div>
</DeliveryStatus>
)}
Expand Down
54 changes: 54 additions & 0 deletions src/features/relay/SelfRelayButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use client';

import { SpinnerIcon } from '@hyperlane-xyz/widgets';
import { useConnectModal } from '@rainbow-me/rainbowkit';
import { useCallback, useEffect, useRef } from 'react';
import { useAccount } from 'wagmi';
import { Message, MessageStatus } from '../../types';
import { useSelfRelay } from './useSelfRelay';

interface SelfRelayButtonProps {
message: Message;
disabled?: boolean;
}

export function SelfRelayButton({ message, disabled }: SelfRelayButtonProps) {
const { isConnected } = useAccount();
const { openConnectModal } = useConnectModal();
const { relay, isRelaying } = useSelfRelay();

const isDelivered = message.status === MessageStatus.Delivered;
const pendingRelayRef = useRef(false);

// Trigger relay after wallet connects
useEffect(() => {
if (isConnected && pendingRelayRef.current) {
pendingRelayRef.current = false;
relay({ message });
}
}, [isConnected, relay, message]);

const handleClick = useCallback(() => {
if (!isConnected) {
pendingRelayRef.current = true;
openConnectModal?.();
return;
}
relay({ message });
}, [isConnected, openConnectModal, relay, message]);

if (isDelivered) {
return null;
}

return (
<button
onClick={handleClick}
disabled={isRelaying || disabled}
className="mt-3 flex items-center justify-center gap-2 rounded-md bg-pink-500 px-3 py-1.5 text-sm font-medium text-white transition-all hover:bg-pink-600 active:bg-pink-700 disabled:bg-gray-300 disabled:text-gray-500"
>
{isRelaying && <SpinnerIcon width={14} height={14} />}
{isRelaying ? 'Relaying...' : 'Self Relay'}
</button>
);
}
3 changes: 3 additions & 0 deletions src/features/relay/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { SelfRelayButton } from './SelfRelayButton';
export { useRelayer } from './useRelayer';
export { useSelfRelay } from './useSelfRelay';
53 changes: 53 additions & 0 deletions src/features/relay/useRelayer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { HyperlaneRelayer } from '@hyperlane-xyz/relayer';
import { HyperlaneCore, MultiProvider } from '@hyperlane-xyz/sdk';
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useReadyMultiProvider, useRegistry, useStore } from '../../store';
import { logger } from '../../utils/logger';

export function useRelayer() {
const multiProtocolProvider = useReadyMultiProvider();
const registry = useRegistry();
const chainMetadata = useStore((s) => s.chainMetadata);

const { data: addresses } = useQuery({
queryKey: ['hyperlane-addresses', registry],
queryFn: async () => {
return registry.getAddresses();
},
enabled: !!registry,
staleTime: Infinity,
});

const evmMultiProvider = useMemo(() => {
if (!multiProtocolProvider || !chainMetadata) return null;
try {
return new MultiProvider(chainMetadata);
} catch (error) {
logger.error('Failed to create MultiProvider:', error);
return null;
}
}, [multiProtocolProvider, chainMetadata]);

const core = useMemo(() => {
if (!evmMultiProvider || !addresses) return null;
try {
return HyperlaneCore.fromAddressesMap(addresses, evmMultiProvider);
} catch (error) {
logger.error('Failed to create HyperlaneCore:', error);
return null;
}
}, [evmMultiProvider, addresses]);

const relayer = useMemo(() => {
if (!core) return null;
return new HyperlaneRelayer({ core, caching: true });
}, [core]);

return {
relayer,
core,
evmMultiProvider,
isReady: !!relayer,
};
}
107 changes: 107 additions & 0 deletions src/features/relay/useSelfRelay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useMutation } from '@tanstack/react-query';
import { providers } from 'ethers';
import { useCallback } from 'react';
import { toast } from 'react-toastify';
import { useAccount } from 'wagmi';
import { Message } from '../../types';
import { logger } from '../../utils/logger';
import { useRelayer } from './useRelayer';

interface SelfRelayParams {
message: Message;
}

function getEthersSigner(): providers.JsonRpcSigner | null {
if (typeof window === 'undefined') return null;
const ethereum = (window as any).ethereum;
if (!ethereum) return null;
const provider = new providers.Web3Provider(ethereum);
return provider.getSigner();
}

export function useSelfRelay() {
const { relayer, evmMultiProvider, isReady } = useRelayer();
const { address, isConnected } = useAccount();

const relayMessage = useCallback(
async ({ message }: SelfRelayParams) => {
if (!relayer || !evmMultiProvider) {
throw new Error('Relayer not initialized');
}

if (!isConnected || !address) {
throw new Error('Wallet not connected');
}

const signer = getEthersSigner();
if (!signer) {
throw new Error('Could not get wallet signer');
}

const { origin, msgId, destinationDomainId } = message;
const destChainName = evmMultiProvider.tryGetChainName(destinationDomainId);

if (!destChainName) {
throw new Error(`Unknown destination chain: ${destinationDomainId}`);
}

logger.debug(`Starting self-relay for message ${msgId} to ${destChainName}`);

const originChainName = evmMultiProvider.tryGetChainName(message.originDomainId);
if (!originChainName) {
throw new Error(`Unknown origin chain: ${message.originDomainId}`);
}

const originProvider = evmMultiProvider.getProvider(originChainName);
const txReceipt = await originProvider.getTransactionReceipt(origin.hash);

if (!txReceipt) {
throw new Error(`Could not fetch transaction receipt for ${origin.hash}`);
}

evmMultiProvider.setSharedSigner(signer);

try {
const result = await relayer.relayMessage(txReceipt);
logger.debug(`Self-relay completed for message ${msgId}`, result);
return result;
} catch (relayError: unknown) {
const errorMessage =
relayError instanceof Error ? relayError.message : 'Unknown relay error';
logger.error(`Relay error for message ${msgId}:`, relayError);

// Re-throw with a user-friendly message
if (errorMessage.includes('Only built') && errorMessage.includes('required modules')) {
throw new Error(
'Validator signatures not yet available. The message may need more time for validators to sign.',
);
} else if (errorMessage.includes('required checkpoints')) {
throw new Error('Waiting for validator checkpoints. Please try again in a few minutes.');
}
throw relayError;
}
},
[relayer, evmMultiProvider, isConnected, address],
);

const mutation = useMutation({
mutationFn: relayMessage,
onSuccess: (data) => {
toast.success('Message relayed successfully!');
logger.debug('Relay transaction:', data);
},
onError: (error: Error) => {
logger.error('Self-relay failed:', error);
toast.error(`Relay failed: ${error.message}`);
},
});

return {
relay: mutation.mutate,
relayAsync: mutation.mutateAsync,
isRelaying: mutation.isPending,
isSuccess: mutation.isSuccess,
error: mutation.error,
isReady: isReady && isConnected,
};
}
Loading