Skip to content

feat: self-relay for EVM messages#247

Draft
paulbalaji wants to merge 29 commits intomainfrom
pb/selfrelay
Draft

feat: self-relay for EVM messages#247
paulbalaji wants to merge 29 commits intomainfrom
pb/selfrelay

Conversation

@paulbalaji
Copy link
Copy Markdown
Collaborator

@paulbalaji paulbalaji commented Dec 25, 2025

Summary

  • restores EVM self-relay on top of perf: keep explorer metadata-first and evm-only at runtime #301's stable metadata-first runtime changes
  • adds RainbowKit/wagmi wallet connection in the explorer shell
  • shows a Self relay action on pending and failing EVM-to-EVM messages
  • uses the active wallet connector plus network switching instead of reading window.ethereum directly

Why stack on #301

This PR originally depended on beta Hyperlane packages and a pile of Vercel/EMFILE workarounds. Rebasing it onto #301 keeps the feature on top of the stable dependency bump and the metadata-first, EVM-only runtime path that is already green, so this PR now carries the self-relay feature itself instead of reintroducing the old beta package surface and build-trace issues.

Dependencies

  • @hyperlane-xyz/core@11.3.1
  • @hyperlane-xyz/relayer@1.1.23
  • @hyperlane-xyz/sdk@32.0.0
  • @hyperlane-xyz/widgets@32.0.0
  • @rainbow-me/rainbowkit@2.2.0
  • wagmi@2.13.0
  • viem@2.39.3
  • @wagmi/connectors@5.5.0

Validation

  • pnpm run typecheck
  • pnpm run lint
  • pnpm run build

Notes

  • pnpm run test still fails on clean pr-301 too because Jest is missing jest-environment-jsdom, and the PI query tests currently choke on ESM in @hyperlane-xyz/tron-sdk. Those failures were not introduced by this PR.

🤖 Generated with Claude Code


Note

Medium Risk
Adds new server-side proxy endpoints and client-side relaying/wallet flows; although the RPC proxy includes hostname/IP allowlisting, mistakes here could enable SSRF or break RPC/relay behavior.

Overview
Restores EVM self-relay in the explorer UI: a new SelfRelayButton appears on pending/failing destination cards for EVM→EVM messages and uses the connected wallet (with automatic chain switching) to submit the relay transaction.

Introduces wallet connectivity via RainbowKit/wagmi (EvmWalletContext, ConnectWalletButton) and wires it into the app shell and header; adds NEXT_PUBLIC_WALLET_CONNECT_ID config to enable WalletConnect.

Adds browser-safe network access helpers: new /api/rpc-proxy (POST JSON-RPC proxy with hostname/IP blocking and optional header extraction) and /api/s3-proxy (GET-only allowlisted S3 JSON proxy), plus client helpers that proxy/normalize chain RPC URLs and patch the SDK’s S3 validator fetches to go through the proxy.

Reviewed by Cursor Bugbot for commit a14dd23. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel
Copy link
Copy Markdown

vercel Bot commented Dec 25, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperlane-explorer Ready Ready Preview, Comment Apr 16, 2026 10:52pm

Request Review

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Dec 25, 2025

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​hyperlane-xyz/​relayer@​1.1.237610010099100

View full report

@paulbalaji
Copy link
Copy Markdown
Collaborator Author

Self-Relay Implementation Status

✅ What Works

  • Self-relay functionality works locally - users can connect wallet and relay pending/failing messages
  • Button UI integrated into TransactionCard for Pending and Failing message states
  • Proper wallet connection flow (connects wallet first if needed, then triggers relay)
  • Error handling for common relay failures (validator signatures not ready, etc.)

❌ Vercel Deployment Issues

The deployment fails with EMFILE: too many open files errors. After extensive debugging, here's what we found:

Root Cause

The beta @hyperlane-xyz/core@10.1.4-beta package contains thousands of typechain factory files that exceed Vercel's serverless function file descriptor limits (~1024).

Attempted Fixes (None Worked)

Attempt Result
Dynamic import SelfRelayButton with ssr: false Still traces dependencies
Dynamic import EvmWalletContext with ssr: false Moved error to @hyperlane-xyz/core
serverExternalPackages in next.config.js Conflicts with transpilePackages
outputFileTracingExcludes for typechain dirs Breaks module resolution (core/index.js imports typechain/index.js)
Remove transpilePackages and optimizePackageImports Still EMFILE errors
Return empty <div> during SSR (like warp-ui-template) Still EMFILE errors
Switch OG API route to nodejs runtime Timeout errors + still EMFILE

Why warp-ui-template Works

The warp-ui-template does not use @hyperlane-xyz/core or @hyperlane-xyz/relayer at all. The explorer imports @hyperlane-xyz/core in 5+ files throughout the codebase (debugger, collateral, ICA, PI queries, message utils), so the large package gets bundled regardless of the self-relay feature.

📋 Recommended Courses of Action

  1. Wait for stable release (Recommended)

    • Keep this PR as draft
    • The stable @hyperlane-xyz/relayer release will likely have better optimization
    • Stable @hyperlane-xyz/core (10.0.5) worked fine on Vercel before
  2. Test locally only

    • Merge with understanding that self-relay only works in local dev
    • Add a note/disable the feature in production builds
  3. Deploy separately

    • Keep explorer on stable packages
    • Deploy self-relay as a standalone service/app
  4. Alternative deployment

    • Try deployment platforms with higher file limits
    • Or use Vercel's higher-tier plans that may have increased limits

🔧 Technical Notes

  • The @hyperlane-xyz/relayer package depends on @hyperlane-xyz/core for HyperlaneCore.fromAddressesMap()
  • The relayer's relayMessage() works but requires MESSAGE_ID_MULTISIG ISM (MERKLE_ROOT_MULTISIG not supported in TS relayer)
  • S3 validator storage fetches may fail due to CORS in browser environments

This comment summarizes findings from debugging session on 2024-12-25

Comment thread src/features/wallet/EvmWalletContext.tsx Outdated
Comment thread src/features/wallet/ConnectWalletButton.tsx
Comment thread src/features/relay/useSelfRelay.ts
Comment thread src/features/relay/useSelfRelay.ts Outdated
Comment thread src/features/relay/useRelayer.ts Outdated
if (first === 192 && second === 168) return true;
if (first === 172 && second >= 16 && second <= 31) return true;

return false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SSRF bypass via IPv6-mapped IPv4 addresses

High Severity

The hasBlockedHostname function doesn't block IPv6-mapped IPv4 addresses like ::ffff:127.0.0.1 or ::ffff:10.0.0.1. After the net.isIP check identifies these as IPv6 (returning 6), they don't match the explicit ::1, fc/fd, or fe80: checks. Then the .split('.').map(Number) logic produces NaN for the first element (e.g. '::ffff:127'), so all IPv4-range comparisons (first === 127, etc.) fail. This allows proxying requests to internal/loopback services via the IPv6-mapped representation.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 14b4ee7. Configure here.


return {
relay: mutation.mutate,
relayAsync: mutation.mutateAsync,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused relayAsync export is dead code

Low Severity

relayAsync (wrapping mutation.mutateAsync) is exported from useSelfRelay but is never consumed anywhere in the codebase. Only relay (mutation.mutate) is actually used by SelfRelayButton. This is dead code that adds unnecessary API surface.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 14b4ee7. Configure here.


parsed.search = '';
retainedParams.forEach(([key, value]) => parsed.searchParams.append(key, value));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RPC proxy header values not sanitized against injection

Medium Severity

The stripCustomRpcHeaders function forwards user-controlled custom_rpc_header query parameters as arbitrary HTTP headers to the upstream server with no header-name blocklist. An attacker can set sensitive headers like Host, X-Forwarded-For, Authorization, or Cookie on requests to any allowed upstream, enabling request smuggling or header injection attacks through this unauthenticated proxy endpoint.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit db05dfd. Configure here.

Comment thread pnpm-lock.yaml
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 5 total unresolved issues (including 3 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a14dd23. Configure here.

penalty: getRpcPenalty(rpcUrl.http),
}))
.sort((a, b) => a.penalty - b.penalty || a.index - b.index)
.map(({ rpcUrl }) => rpcUrl);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant filtering makes normalizeRpcUrls sort a no-op

Low Severity

normalizeRpcUrls calls filterPreferredRpcUrls first, which removes all deprioritized URLs (penalty > 0). It then calls getRpcPenalty on the already-filtered results — where every item has penalty 0 — making the .sort() by penalty meaningless. The sort-by-penalty logic can never differentiate any items, so it's dead code that only preserves insertion order.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a14dd23. Configure here.

}

const signer = await getEthersSigner(connector);
if (!signer) throw new Error('Could not get wallet signer');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signer fetched after chain switch may be stale

Medium Severity

After switchChainAsync completes, getEthersSigner(connector) is called with the connector reference captured before the chain switch. The connector's underlying provider may not yet reflect the new chain, potentially producing a signer connected to the wrong network. The signer retrieval needs to occur after the connector has fully settled on the new chain.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a14dd23. Configure here.

Copy link
Copy Markdown
Collaborator Author

Quick status update on the latest preview:

We do not appear to be hitting the earlier size-related failure anymore. The preview now gets through the previous transport/bundle path and fails later in the self-relay flow.

The remaining blocker is still metadata construction for some messages. The current failure mode is the relayer returning Unable to build metadata on a nested aggregation ISM path, so the issue now looks like metadata-build/runtime-provider behavior rather than the old size problem.

I’m continuing to narrow that down, but wanted to note that this no longer looks like the original size issue.

@paulbalaji paulbalaji force-pushed the pbio/perf-beta-package-surfaces branch 2 times, most recently from 5e36845 to 5677acf Compare April 21, 2026 15:45
Base automatically changed from pbio/perf-beta-package-surfaces to main April 21, 2026 17:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant