Skip to content

feat: metaswap demo v2#945

Open
ameten wants to merge 151 commits intomainfrom
metaswap-demo-2
Open

feat: metaswap demo v2#945
ameten wants to merge 151 commits intomainfrom
metaswap-demo-2

Conversation

@ameten
Copy link
Contributor

@ameten ameten commented Feb 12, 2026

Summary

Continuation of #939 — Warp UI integration for Metaswaps demo (Optimism→Base USDC with ICA commit-reveal).

Related

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Swap Bridge functionality for cross-chain token transfers with integrated fee quoting.
    • Introduced Interchain Account (ICA) support as an alternative transfer method.
    • Enhanced token selection with improved search and filtering capabilities.
    • Added mobile-responsive navigation menu with dropdown support.
  • Style

    • Redesigned header and footer layouts for better responsiveness.
    • Updated color palette, gradients, and visual styling throughout the interface.
    • Implemented custom font loading for improved typography.
  • Improvements

    • Enhanced address validation and recipient handling for transfers.
    • Improved fee transparency and balance display.

Xaroz and others added 30 commits December 5, 2025 13:36
chore: change token identification to use an identifier instead of tokenIndex
@ameten ameten requested a review from Xaroz as a code owner February 12, 2026 21:31
@vercel
Copy link

vercel bot commented Feb 12, 2026

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

Project Deployment Actions Updated (UTC)
hyperlane-warp-template Ready Ready Preview, Comment Feb 12, 2026 10:13pm
5 Skipped Deployments
Project Deployment Actions Updated (UTC)
analytics-test Ignored Ignored Feb 12, 2026 10:13pm
injective-bridge Ignored Ignored Feb 12, 2026 10:13pm
nexus-bridge Ignored Ignored Feb 12, 2026 10:13pm
ousdt-bridge Ignored Ignored Feb 12, 2026 10:13pm
trump-bridge Ignored Ignored Feb 12, 2026 10:13pm

Request Review

@socket-security
Copy link

socket-security bot commented Feb 12, 2026

@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

Warning

Rate limit exceeded

@ameten has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 42 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This pull request substantially refactors the token management system from index-based to key-based lookup (chain-symbol-address), introduces a new swap-bridge feature with Interchain Account support, restructures navigation and form components, adds AWS S3 font distribution, and applies sweeping UI/styling updates across the application.

Changes

Cohort / File(s) Summary
Configuration & Environment
.env.example, .gitignore, eslint.config.mjs, next.config.js, package.json
AWS S3 environment variables added, public/fonts directory excluded from version control, ESLint rule for React Hooks downgraded to warning, Sentry integration removed from Next.js config with outputFileTracingExcludes and experimental build options added, new AWS SDK and font-fetching dependencies introduced.
Font & Asset Infrastructure
scripts/fetch-fonts.mjs, src/styles/globals.css
New Node.js script to download custom fonts (PPValve, PPFraktionMono) from S3 bucket; custom @font-face declarations and scrollbar styling added to global CSS.
Icon Components
src/components/icons/...*
Eight new SVG icon components added: BookIcon, ChevronLargeIcon, HamburgerIcon, HyperlaneGradientLogo, HyperlaneTransparentLogo, StakeIcon, SwapIcon, WebSimpleIcon, XIcon; all memoized for performance.
Navigation & Layout Restructuring
src/components/nav/Header.tsx, src/components/nav/Footer.tsx, src/components/nav/Nav.tsx, src/components/layout/ModalHeader.tsx, src/components/nav/FloatingButtonStrip.tsx, src/pages/index.tsx
Header converted to responsive mobile/desktop layout with DropdownMenu and HamburgerIcon; Footer simplified to use new NavItem components; new Nav.tsx defines navLinks and NavItem; FloatingButtonStrip removed; TipCard repositioned to absolute layout on xl screens; ModalHeader component added.
Token Management System Refactor
src/features/tokens/hooks.ts, src/features/tokens/utils.ts, src/features/tokens/types.ts, src/features/tokens/balances.ts, src/features/tokens/SelectTokenIdField.tsx, src/features/tokens/SelectOrInputTokenIds.tsx
Token lookup switched from index-based to key-based (chain-symbol-address); new utilities: getTokenKey, dedupeTokensByCollateral, buildTokensArray, groupTokensByCollateral, checkTokenHasRoute, findRouteToken; TokenSelectionMode type added; balance hooks updated to accept Token instead of TransferFormValues.
Token Selection UI
src/features/tokens/TokenSelectField.tsx, src/features/tokens/TokenList.tsx, src/features/tokens/TokenListPanel.tsx, src/features/tokens/UnifiedTokenChainModal.tsx, src/features/tokens/TokenListModal.tsx, src/features/tokens/ImportTokenButton.tsx, src/features/tokens/TokenChainIcon.tsx, src/features/tokens/useTokenPrice.tsx
TokenSelectField refactored to use new token-key-based flow with TokenChainIcon and UnifiedTokenChainModal; new TokenList and TokenListPanel components for searchable, chainable token selection; old TokenListModal removed; ImportTokenButton added for wallet token imports; useTokenPrice updated to use token keys.
Chain Selection UI
src/features/chains/ChainFilterPanel.tsx, src/features/chains/ChainList.tsx, src/features/chains/MobileChainQuickSelect.tsx, src/features/chains/hooks.ts, src/features/chains/ChainSelectField.tsx, src/features/chains/ChainSelectModal.tsx
New ChainFilterPanel, ChainList, and MobileChainQuickSelect components added for responsive chain selection; useChainInfos hook introduced; old ChainSelectField and ChainSelectModal removed entirely.
Swap & Bridge Feature
src/features/swap/swapConfig.ts, src/features/swap/icaFees.ts, src/features/swap/components/IcaBalanceDisplay.tsx, src/features/swap/components/IcaPanel.tsx, src/features/swap/components/IcaSendForm.tsx, src/features/swap/hooks/useIcaAddress.ts, src/features/swap/hooks/useIcaBalance.ts, src/features/swap/hooks/useIcaTransaction.ts, src/features/swap/hooks/useInterchainAccount.ts, src/features/swap/hooks/useSwapQuote.ts
Complete swap-bridge infrastructure with demo chain configs (Optimism↔Base), ICA commitment/reveal fee calculations, balance fetching, address resolution, transaction building for multi-call operations (approve + warpRoute.transferRemote), and swap quote aggregation.
Transfer Form Refactoring
src/features/transfer/TransferTokenForm.tsx, src/features/transfer/TransferTokenCard.tsx, src/features/transfer/types.ts, src/features/transfer/useTransferRoute.ts, src/features/transfer/useSwapBridgeTransfer.ts, src/features/transfer/useTokenTransfer.ts, src/features/transfer/useFeeQuotes.ts, src/features/transfer/TransferSection.tsx
TransferFormValues refactored to use originTokenKey/destinationTokenKey instead of origin/destination/tokenIndex; new TransferRouteType (warp/swap-bridge/unavailable) and useTransferRoute hook for route detection; executeSwapBridge orchestrates end-to-end swap-bridge flow with fee buffering and commitment posting; new TransferSection component for section headers.
Transfer UI Components
src/components/buttons/ConnectAwareSubmitButton.tsx, src/components/buttons/SolidButton.tsx, src/components/banner/WarningBanner.tsx, src/components/banner/FormWarningBanner.tsx, src/components/input/SearchInput.tsx, src/components/input/TextField.tsx, src/components/tip/TipCard.tsx
ConnectAwareSubmitButton disabled logic tightened; SolidButton gradient and glow styling added; WarningBanner rounded corners applied; FormWarningBanner offset adjusted; SearchInput component introduced; TextField defaults simplified; TipCard redesigned with gradient container and HyperlaneTransparentLogo.
Wallet & Recipient Management
src/features/wallet/WalletDropdown.tsx, src/features/wallet/RecipientAddressModal.tsx, src/features/wallet/SideBarMenu.tsx, src/features/wallet/ConnectWalletButton.tsx, src/features/wallet/context/SolanaWalletContext.tsx, src/features/transfer/RecipientConfirmationModal.tsx
New WalletDropdown component with recipient address modal integration; RecipientAddressModal added for address input validation; SideBarMenu enhanced with backdrop overlay and Escape key handling; ConnectWalletButton updated with gradient styling; Phantom and Backpack wallets removed from Solana context; recipient resolution logic improved in confirmation modals.
Constants & Configuration
src/consts/app.ts, src/consts/args.ts, src/consts/config.ts, src/consts/links.ts, src/consts/warpRouteWhitelist.ts, src/features/store.ts, src/features/warpCore/warpCoreConfig.ts
BACKGROUND_COLOR and BACKGROUND_IMAGE updated with gradients, MAIN_FONT removed; WARP_QUERY_PARAMS split TOKEN into ORIGIN_TOKEN and DESTINATION_TOKEN; Config interface updated to use token keys instead of chain names; explorer link and stake link added; warpRouteBlacklist introduced for route filtering; store extended with tokens, collateralGroups, tokenByKeyMap, and isTipCardActionTriggered state.
Analytics & Fees
src/features/analytics/utils.ts, src/features/transfer/fees.ts, src/features/transfer/fees.test.ts, src/features/transfer/maxAmount.ts, src/features/transfer/FeeSectionButton.tsx, src/features/transfer/TransferFeeModal.tsx, src/features/transfer/TransfersDetailsModal.tsx
Token lookup in analytics shifted to key-based; fee calculation updated with route-aware token resolution; test helper modified to accept originToken; maxAmount flow integrated with recipient and route validation; FeeSectionButton simplified with loading-dot animation; TransferFeeModal header refactored with ModalHeader; TransfersDetailsModal redesigned for compact layout.
Styling & Theming
tailwind.config.js, src/features/WarpContextInitGate.tsx, src/pages/_app.tsx, src/pages/_document.tsx
Tailwind theme restructured: fontFamily.primary/secondary (PP Valve, PP Fraktion Mono) replaces sans/mono; color scales expanded with cream and new gray stops; new backgroundImage gradients (app, accent, error, card, tip-card); new boxShadow and dropShadow tokens; BRAND_COLOR and BACKGROUND_COLOR used in WarpContextInitGate; font class references updated in _app and _document.
Infrastructure & Instrumentation
src/instrumentation.ts, src/features/chains/ChainConnectionWarning.tsx, next.config.js
Sentry initialization made async with dynamic import and early return when DSN missing; ChainConnectionWarning modal removed; NEXT_PUBLIC_SENTRY_DSN exposed in Next.js config env; next.config.js simplified to use bundle analyzer only.
Testing & Utils
src/features/tokens/utils.test.ts, src/features/transfer/utils.ts, src/features/tokens/SelectOrInputTokenIds.tsx
Extensive test suite added for dedupeTokensByCollateral and buildTokensArray with edge cases; shouldClearAddress utility added for protocol-aware address validation; tokenIndex references removed from form components.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as TransferForm
    participant Swap as SwapBridge Engine
    participant Origin as Origin Chain
    participant Router as Universal Router
    participant ICA as ICA Commitment
    participant Dest as Destination Chain
    participant Relay as Relayer Service

    User->>UI: Select origin token, destination, amount
    UI->>Swap: executeSwapBridge(params)
    Swap->>Origin: Switch chain context
    Swap->>Router: Quote swap (origin→bridge token)
    Router-->>Swap: Swap output amount
    Swap->>Dest: Quote bridge fee + ICA fee
    Dest-->>Swap: Fee amounts
    Swap->>Swap: Build ICA commitment (approve+transferRemote)
    Swap->>Origin: Check token approval needed
    alt Approval Required
        Swap->>Router: Approve bridge token spend
        Router-->>Swap: Approval confirmed
    end
    Swap->>Router: Simulate swap+bridge tx with fee buffer
    Swap->>Router: Send transaction (swap→bridge→ICA)
    Router-->>Swap: TX receipt
    Swap->>Relay: Post ICA commitment payload
    Relay-->>Swap: Commitment stored
    Swap-->>UI: Success (txHash)
    UI-->>User: Show confirmation
    Note over Dest: ICA executor processes commit/reveal
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

Ogres are like onions, layers and layers—
Tokens swapped, routes layered, fees no deniers,
From chains to wallets, a new bridge so fair,
ICA commitments floating through the air,
Refactored and styled, with gradients grand,
A proper journey across the blockchain land. 🧅✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: metaswap demo v2' clearly and concisely summarizes the primary change: implementing a version 2 of the metaswap demo feature.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch metaswap-demo-2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/features/transfer/useTokenTransfer.ts (1)

359-376: ⚠️ Potential issue | 🟠 Major

origin is not accessible in the catch block — pre-existing scoping issue now affects swap-bridge path too

This is like finding an onion with extra layers, aye? The const origin declarations (line 177 in the swap-bridge if-block, line 239 in the try body) are block-scoped and not visible inside the catch. If an error is thrown after the origin assignment, the reference at line 371 (getChainDisplayName(multiProvider, origin)) would throw a ReferenceError, masking the real error.

This was pre-existing, but the new swap-bridge path makes it more likely to be hit. Consider hoisting origin with let before the try block.

🐛 Proposed fix
+  let origin: string | undefined;
   try {
     const originToken = routeOverrideToken || getTokenByKey(warpCore.tokens, originTokenKey);
     const destinationToken = getTokenByKey(warpCore.tokens, destinationTokenKey);
     if (!originToken || !destinationToken) throw new Error('No token route found between chains');
     ...
     if (routeType === 'swap-bridge') {
-      const origin = originToken.chainName;
+      origin = originToken.chainName;
       ...
     }
-    const origin = originToken.chainName;
+    origin = originToken.chainName;
     ...
   } catch (error: any) {
     ...
-      toast.error(
-        `Transaction timed out, ${getChainDisplayName(multiProvider, origin)} may be busy...`
-      );
+      toast.error(
+        `Transaction timed out, ${origin ? getChainDisplayName(multiProvider, origin) : 'the chain'} may be busy...`
+      );
     ...
   }
src/features/chains/ChainConnectionWarning.tsx (1)

1-7: ⚠️ Potential issue | 🔴 Critical

ChainName type is used but not imported — this'll cause a compilation error, donkey.

Lines 13-14 reference ChainName in the component props, but it's nowhere in the import block. Likely needs to come from @hyperlane-xyz/sdk.

🐛 Proposed fix
-import { ChainMetadata, isRpcHealthy } from '@hyperlane-xyz/sdk';
+import { ChainMetadata, ChainName, isRpcHealthy } from '@hyperlane-xyz/sdk';
#!/bin/bash
# Verify how ChainName is typically imported in this codebase
rg -n "import.*ChainName" --type=ts --type=tsx -g '!node_modules' | head -20

Also applies to: 13-14

src/features/transfer/TransferFeeModal.tsx (1)

22-73: ⚠️ Potential issue | 🟡 Minor

Skeletons won't show during initial load when fees is null.

Now look — the skeleton placeholders are nested inside conditionals like fees?.localQuote && fees.localQuote.amount > 0n. When fees is null (initial loading state), none of these blocks render at all, so the user sees an empty modal with just the "Read more" link. If the intent is to show loading indicators while fees are being fetched, the skeletons need to be rendered independently of the fees checks.

Something like:

-        {fees?.localQuote && fees.localQuote.amount > 0n && (
+        {(isLoading || (fees?.localQuote && fees.localQuote.amount > 0n)) && (

Or restructure so that when isLoading && !fees, a generic loading skeleton is displayed.

src/features/transfer/fees.ts (1)

140-151: ⚠️ Potential issue | 🟠 Major

Pass originRouteToken instead of originToken to tryGetDefaultOriginToken.

The code explicitly calls findRouteToken to resolve the correct token after deduplication (lines 125-127), then builds tokensWithSameCollateralAddresses using that resolved originRouteToken (lines 128-130). However, the lookup at line 142 passes the original originToken instead. Since the function uses the token's collateralAddressOrDenom and chain name for config lookups, passing a deduplicated token that differs from the one used to build the candidates array will cause the default route matching to fail.

🤖 Fix all issues with AI agents
In `@src/features/chains/ChainFilterPanel.tsx`:
- Around line 1-9: The prop type ChainName used in the ChainFilterPanelProps
(for selectedChain and onSelectChain) is not imported; add the missing named
import for ChainName (e.g. import { ChainName } from '<module where ChainName is
defined>') at the top of ChainFilterPanel.tsx so the ChainFilterPanelProps type
compiles; ensure the import is a named import and matches the module that
exports ChainName.

In `@src/features/swap/hooks/useIcaAddress.ts`:
- Around line 16-17: The queryKey for useQuery currently includes the class
instance icaApp which is unstable; remove icaApp from the queryKey and use a
stable primitive instead (e.g., a unique id/hash from the object or simply omit
it and rely on the existing enabled guard). Update the useQuery call that builds
queryKey=['icaAddress', icaApp, userAddress, originChainName,
destinationChainName] to exclude icaApp or replace it with icaApp?.id or a
deterministic configHash, and ensure the enabled option uses Boolean(icaApp) so
the query only runs when the instance is available; keep function names like
useIcaAddress and useQuery unchanged.

In `@src/features/swap/hooks/useIcaTransaction.ts`:
- Around line 186-190: The code in useIcaTransaction sets status to 'complete'
unconditionally even when publicClient is undefined, so change the post-submit
flow to reflect unverifiable transactions: after attempting to wait for receipt
using publicClient.waitForTransactionReceipt({ hash, confirmations: 1 }), only
setStatus('complete') if publicClient existed and the receipt was successfully
awaited; if publicClient is undefined (or the await throws/no receipt),
setStatus('confirming') or 'submitted' instead (or add a new 'submitted' state)
and ensure any error path keeps status as 'confirming' so users know the tx is
not confirmed; update references to setStatus and any UI consumers expecting
'complete' accordingly.
- Around line 83-103: The code currently defaults msgFee to 0n after calling
destinationPublicClient.readContract(quoteTransferRemote) which can silently
allow sending zero native value; after the for-loop in useIcaTransaction where
msgFee and tokenPull are computed, add a guard that detects msgFee === 0n (and
optionally quotes.length > 0) and either throw a descriptive error or emit a
diagnostic log before proceeding (include function/variable names like
quoteTransferRemote, msgFee, tokenPull, destConfig.icaBridgeRoute,
destinationPublicClient.readContract) so the caller sees that no native fee was
quoted and the transaction should not send value 0. Ensure the thrown error
message includes the original quote response context to aid debugging.

In `@src/features/swap/hooks/useInterchainAccount.ts`:
- Around line 18-25: The catch is silently swallowing errors in
useInterchainAccount when InterchainAccount.fromAddressesMap fails; update the
catch to log the error (including context like addressesMap and
multiProtocolProvider.metadata) via console.error or process logger and then
rethrow (or return null only for expected/handled cases), referencing the
InterchainAccount.fromAddressesMap call and the MultiProvider construction (new
MultiProvider(multiProtocolProvider.metadata)) so failures are visible for
debugging.

In `@src/features/swap/swapConfig.ts`:
- Around line 83-110: The SWAP_CHAIN_CONFIGS object contains mixed-case
(checksummed) EVM addresses; update the hardcoded fields (universalRouter,
quoterV2, icaBridgeRoute, wrappedNative, etc.) to be lowercase and ensure any
registry-derived values returned by functions like
requireIcaRouter(DEMO_ORIGIN_CHAIN) / requireIcaRouter(DEMO_DESTINATION_CHAIN)
are normalized (call .toLowerCase() on their result or within the function)
before assignment so all EVM addresses in SWAP_CHAIN_CONFIGS are lowercase while
preserving case-sensitive non-EVM values.

In `@src/features/tokens/hooks.ts`:
- Around line 29-35: findTokenByChainSymbol currently uses
chainSymbol.split('-') which breaks when chain names contain hyphens; change the
parsing to find the last dash and split there (use
chainSymbol.lastIndexOf('-')), return undefined if no dash, set chainName =
chainSymbol.slice(0, lastDash) and symbol = chainSymbol.slice(lastDash + 1),
then perform the same comparison (t.chainName === chainName &&
t.symbol.toLowerCase() === symbol.toLowerCase()) so tokens with hyphenated chain
names (e.g., "arbitrum-nova-USDC") are handled correctly.

In `@src/features/tokens/ImportTokenButton.tsx`:
- Around line 20-24: In the catch block in ImportTokenButton.tsx (the block
referencing USER_REJECTED_ERROR), stop logging all errors at debug level: when
the error is NOT a user-rejection (i.e., if
!errorDetails.includes(USER_REJECTED_ERROR)) call logger.error(error) (so it
surfaces to Sentry) and show toast.error(errorDetails) as you already do; only
for user-rejected cases keep the quieter path (logger.debug or no toast). Leave
the USER_REJECTED_ERROR check, the toast.error call for non-rejections, and
ensure you do not suppress or swallow unexpected errors by keeping logger.error
for those cases.

In `@src/features/transfer/TransferTokenForm.tsx`:
- Around line 1018-1043: getRouteType duplicates the route-classification logic
in useTransferRoute; extract the pure decision logic into a single shared helper
(e.g., determineTransferRoute or computeRouteType) that accepts the same inputs
(tokens, collateralGroups, values or the minimal inputs: originToken,
destinationToken, collateralGroups) and returns TransferRouteType, then update
both useTransferRoute and getRouteType to call that helper; ensure the helper
uses the same calls already present (getTokenByKey, checkTokenHasRoute,
isSwapSupported, getSwappableAddress, isDemoSwapBridgePath) and keep public
signatures compatible with TransferFormValues/TransferRouteType so callers need
only replace direct logic with a single call to the new function.
- Around line 114-123: In validate (used with TransferFormValues) after you
detect routeType === 'swap-bridge', in addition to checking values.amount ensure
there is a valid recipient by verifying either values.recipient is
present/non-empty or the derived ICA/destination wallet is available (the logic
that derives ICA address or checks connected destination wallet should be
consulted here); if neither exists return a validation error (e.g. { recipient:
'Recipient required' }) so the form cannot submit with an empty/undefined
recipient. Use getRouteType, values.amount and values.recipient/ICA-derivation
check to locate where to add this check.

In `@src/features/transfer/types.ts`:
- Around line 17-19: The new enum values PostingCommitment, SigningSwapBridge,
and ConfirmingSwapBridge were added but not handled downstream; update
getTransferStatusLabel() to return human-readable labels for those three values,
and add the new statuses to the appropriate status sets (either
SentTransferStatuses and/or FinalTransferStatuses) so finality logic recognizes
swap-bridge flows—specifically ensure ConfirmingSwapBridge is treated as final
(or Sent if you have a separate in-flight concept) so
TransfersDetailsModal.isFinal, STATUSES_WITH_ICON (used by SideBarMenu), the
wallet reconnection check in utils.ts, and failUnconfirmedTransfers() in
store.ts behave correctly; if you decide PostingCommitment should remain
in-flight, include it in SentTransferStatuses but not FinalTransferStatuses, and
ensure SigningSwapBridge is classified consistently with its lifecycle.

In `@src/features/transfer/useSwapBridgeTransfer.ts`:
- Around line 393-414: The call to shareCallsWithPrivateRelayer in the
onStatusChange(TransferStatus.PostingCommitment) block can hang indefinitely;
modify this call to enforce a timeout and surface a clear recovery error: create
an AbortController (or use a timeout wrapper) and pass its signal into
shareCallsWithPrivateRelayer if the function supports it, or wrap the promise in
a Promise.race with a timeout that aborts/throws after a configurable duration
(e.g., 10–30s); on timeout, throw a descriptive error that includes context
(commitment hash, originDomain via buildPostCallsPayload and
COMMITMENTS_SERVICE_URL) and use toErrorMessage for consistency so callers can
present a specific recovery message instead of hanging.
- Around line 107-135: The parameter publicClient in checkAndApprove is typed as
any, losing type safety for publicClient.readContract and
publicClient.waitForTransactionReceipt; change the publicClient parameter type
to viem's PublicClient (import { PublicClient } from 'viem' or your project's
viem wrapper) and update the function signature async function
checkAndApprove(walletClient: WalletClient, publicClient: PublicClient,
tokenAddress: Address, spender: Address, amount: bigint): Promise<void>, leaving
calls to publicClient.readContract and publicClient.waitForTransactionReceipt
as-is so TypeScript can validate their return types and args (ensure
createPublicClient is used where the client is created so the type lines up).
🟡 Minor comments (19)
src/consts/links.ts-3-3 (1)

3-3: ⚠️ Potential issue | 🟡 Minor

This explorer URL looks like a temporary branch preview deployment.

hyperlane-explorer-git-pb-offsite-swap-abacus-works.vercel.app is a Vercel git-branch preview URL. Once that branch gets merged or deleted, this link will go stale faster than an onion left in the sun. If this is intentional for a demo, consider leaving a comment noting it's temporary; otherwise, swap it for the stable production explorer URL.

.env.example-8-8 (1)

8-8: ⚠️ Potential issue | 🟡 Minor

Missing trailing newline at end of file.

Some tools and POSIX standards expect a final newline. Static analysis flagged this too. Small thing, but easy to fix.

Proposed fix
 AWS_REGION=us-east-1
+
src/consts/warpRouteWhitelist.ts-11-16 (1)

11-16: ⚠️ Potential issue | 🟡 Minor

Substring matching with 'form' will unintentionally exclude routes containing that substring.

The filter at line 125 of warpCoreConfig.ts uses .includes() for case-insensitive matching, so the blacklist entry 'form' will match any route ID containing "form" as a substring — think 'reform', 'transform', 'platform', 'perform', etc. If you're just trying to block Form chain routes, you'd want to be more specific, like 'form/' or match against the chain name separately rather than the route ID.

src/components/input/TextField.tsx-32-33 (1)

32-33: ⚠️ Potential issue | 🟡 Minor

Removing padding and font-size from the shared default breaks two out of three consumers.

Stripping mt-1.5 px-2.5 py-2.5 text-sm from defaultClassName means every TextField and TextInput loses its padding and font sizing by default. Checking the actual usages:

  • SelectOrInputTokenIds.tsx: passes only className="w-full" — now renders with zero padding
  • TransferTokenForm.tsx: passes text-xl but no padding classes — loses all default spacing
  • SearchInput.tsx: passes all:py-2 all:text-sm all:border-gray-300 — this one's covered

So two of the three consumers will render with missing padding and visually inconsistent sizing unless they're updated too. That's the kind of change that ripples through everywhere else silently.

package.json-125-126 (1)

125-126: ⚠️ Potential issue | 🟡 Minor

Prebuild font-fetching could break CI if credentials are missing.

The prebuild script runs node scripts/fetch-fonts.mjs before every build. If this script requires AWS S3 credentials (implied by the @aws-sdk/client-s3 devDependency) and they're not configured in the build environment, the build will fail. Consider making this step graceful — the CSS already notes the fonts are "optional" and fall back to system fonts.

src/features/transfer/maxAmount.ts-42-49 (1)

42-49: ⚠️ Potential issue | 🟡 Minor

Recipient fallback to origin sender address may be invalid for cross-protocol transfers.

When formRecipient and connectedDestAddress are both empty, the fallback is address — the sender's origin chain address. For same-protocol transfers (EVM→EVM) this is fine, but for cross-protocol transfers (e.g., EVM→Cosmos, EVM→Solana), the origin address is not a valid recipient on the destination chain. This could cause the fee estimation to fail or return incorrect results.

If this is intentionally a best-effort estimate for UI purposes, consider adding a brief comment explaining the rationale. Otherwise, returning undefined (skip max amount calculation) when no valid destination recipient exists might be safer, like staying in your own swamp when you don't know the way.

scripts/fetch-fonts.mjs-52-55 (1)

52-55: ⚠️ Potential issue | 🟡 Minor

response.Body could be undefined — guard before streaming

The S3 GetObjectCommand response can have an undefined Body (e.g., if the key doesn't exist and no error is thrown, or in certain SDK edge cases). Calling .transformToWebStream() on undefined will throw an unhandled TypeError that the outer catch will swallow with a vague message.

🛡️ Proposed fix
       const response = await s3.send(command);
+      if (!response.Body) {
+        throw new Error(`S3 returned empty body for ${fontFile}`);
+      }
       const writeStream = createWriteStream(outputPath);
 
       await pipeline(Readable.fromWeb(response.Body.transformToWebStream()), writeStream);
tailwind.config.js-124-130 (1)

124-130: ⚠️ Potential issue | 🟡 Minor

accent-glow and error-glow should both use accent.100 per design system v2

Right now accent-glow uses a hardcoded rgba(154, 13, 255, 0.35) (a primary purple) while error-glow correctly references theme('colors.accent.100'). These should use the same color — the idea being you distinguish error states via the error-gradient background, not the glow color. Also, using a hardcoded rgba instead of a theme token makes the accent-glow drift if the palette ever changes.

🎨 Proposed fix
       boxShadow: ({ theme }) => ({
-        'accent-glow': `inset 0 0 20px 0 rgba(154, 13, 255, 0.35)`,
+        'accent-glow': `inset 0 0 20px 0 ${theme('colors.accent.100')}59`,
         'error-glow': `inset 2px 2px 13px 2px ${theme('colors.accent.100')}`,

Based on learnings: "In v2 of the design system, define both accent-glow and error-glow box shadows to use the same color (accent.100). Distinguish error states using the error-gradient background instead of changing the glow color."

src/features/transfer/useTokenTransfer.ts-173-233 (1)

173-233: ⚠️ Potential issue | 🟡 Minor

sender from wagmi may be checksummed — should be lowercased for EVM

At line 179, walletClient.account?.address returns a checksummed (mixed-case) EVM address. Per your coding guidelines, EVM addresses should be lowercase. This sender is stored in the transfer record at line 189 and potentially used downstream. Same goes for originSwapAddress if it derives from checksummed SDK values.

🛡️ Proposed fix
       const sender = walletClient.account?.address;
       if (!sender) throw new Error('No active account found');
+      const normalizedSender = sender.toLowerCase();
 
       addTransfer({
         timestamp: new Date().getTime(),
         status: TransferStatus.Preparing,
         origin,
         destination,
         originTokenAddressOrDenom: originToken.addressOrDenom,
         destTokenAddressOrDenom: destinationToken.addressOrDenom,
-        sender,
+        sender: normalizedSender,
         recipient,
         amount,
       });

As per coding guidelines: "Only lowercase EVM addresses; Solana and Cosmos addresses are case-sensitive."

src/features/transfer/RecipientConfirmationModal.tsx-29-29 (1)

29-29: ⚠️ Potential issue | 🟡 Minor

Empty recipient could show a blank confirmation bubble.

If both values.recipient and connectedDestAddress are falsy, recipient becomes '' and line 41 renders an empty styled <p>. The modal asks "Is this address correct?" while showing... nothing. Might want a guard or at least a fallback display, so the user isn't staring into the void.

Also applies to: 41-41

src/features/tokens/ImportTokenButton.tsx-10-11 (1)

10-11: ⚠️ Potential issue | 🟡 Minor

Type alignment check: Token prop should match useAddToken parameter type.

The useAddToken hook declares its parameter as IToken, but the component's token prop is typed as Token. While both come from the @hyperlane-xyz/sdk library and the code runs fine (suggesting structural compatibility), aligning the prop type with what the hook expects—either IToken or verifying that Token extends IToken—would make the types consistent throughout the feature.

src/components/nav/Header.tsx-48-68 (1)

48-68: ⚠️ Potential issue | 🟡 Minor

Absolute-positioned nav and wallet button may overlap the centered logo on mid-size screens.

The nav (sm:absolute sm:left-6) and wallet button (sm:absolute sm:right-6) use absolute positioning, while the logo is flow-centered. On screens around the sm breakpoint (~640px), these could collide with the logo. Consider testing at intermediate viewport widths or adding a min-width / lg-only gate for the nav.

src/features/wallet/WalletDropdown.tsx-53-58 (1)

53-58: ⚠️ Potential issue | 🟡 Minor

EVM addresses should be lowercased before saving.

The onSaveRecipient callback passes the address straight through from RecipientAddressModal without lowercasing it. The RecipientAddressModal only trims the input. Per the coding guidelines, EVM addresses must be lowercase. This could cause comparison mismatches downstream (e.g., line 65 where recipient is compared to connectedAddress).

🔧 Suggested fix
   const onSaveRecipient = useCallback(
     (address: string) => {
-      onRecipientChange?.(address);
+      onRecipientChange?.(address.toLowerCase());
     },
     [onRecipientChange],
   );

As per coding guidelines: "Only lowercase EVM addresses; Solana and Cosmos addresses are case-sensitive." Since the protocol is known here, you could conditionally lowercase only for EVM, but if this component is EVM-only for now, a simple .toLowerCase() does the job.

src/features/tokens/TokenListPanel.tsx-44-50 (1)

44-50: ⚠️ Potential issue | 🟡 Minor

aria-label prop is silently dropped by SearchInput.

Looking at SearchInput in src/components/input/SearchInput.tsx, its interface only accepts inputRef, value, onChange, and placeholder — there's no aria-label passthrough. So aria-label="Search tokens" here does nothing. The input relies solely on placeholder for its accessible name, which isn't ideal since placeholder text disappears on focus in some screen readers.

Either add aria-label support to SearchInput or use the placeholder as the sole accessible hint (current behavior).

src/features/swap/hooks/useIcaTransaction.ts-101-101 (1)

101-101: ⚠️ Potential issue | 🟡 Minor

ESLint: unused caught error variable — prefix with underscore.

Static analysis flags this. Quick fix to keep the linter happy.

Proposed fix
-         } catch (error) {
+         } catch (_error) {
src/features/wallet/RecipientAddressModal.tsx-24-36 (1)

24-36: ⚠️ Potential issue | 🟡 Minor

EVM addresses should be lowercased before saving.

When protocol is Ethereum, onSave(trimmedAddress) should lowercase the address. For Solana/Cosmos it must be preserved as-is. Right now user-pasted checksummed addresses get stored in mixed case.

As per coding guidelines: "Only lowercase EVM addresses; Solana and Cosmos addresses are case-sensitive."

Proposed fix
    setError('');
-   onSave(trimmedAddress);
+   onSave(protocol === ProtocolType.Ethereum ? trimmedAddress.toLowerCase() : trimmedAddress);
    close();
src/features/chains/ChainFilterPanel.tsx-36-41 (1)

36-41: ⚠️ Potential issue | 🟡 Minor

aria-label is not part of SearchInput's prop interface.

Looking at the SearchInput component in src/components/input/SearchInput.tsx, it accepts { inputRef, value, onChange, placeholder } — no aria-label. This extra prop will be flagged by TypeScript as an invalid prop. If you want the aria attribute on the underlying <input>, you'd need to pass it through or extend SearchInput's props.

src/features/transfer/TransfersDetailsModal.tsx-203-218 (1)

203-218: ⚠️ Potential issue | 🟡 Minor

Dead isFailed check in the loading branch — this ogre's nose can smell unreachable code.

On line 207, isFailed ? 'text-red-600' : 'text-gray-600' will never evaluate to 'text-red-600' because this entire block is inside the !isFinal branch (line 173's else). Since isFinal = isSent || isFailed (line 104), being in !isFinal means isFailed is always false here.

If the intention is to show a red error state for failed transfers in this view, the condition needs restructuring. If it's just leftover, simplify to 'text-gray-600'.

Proposed simplification
-            <div
-              className={`mt-5 text-center text-sm ${isFailed ? 'text-red-600' : 'text-gray-600'}`}
-            >
+            <div className="mt-5 text-center text-sm text-gray-600">
src/features/tokens/TokenSelectField.tsx-159-162 (1)

159-162: ⚠️ Potential issue | 🟡 Minor

Tailwind class typo: cursor pointer should be cursor-pointer.

Missing the hyphen on line 161. Without it, Tailwind won't apply the pointer cursor on enabled state. Small thing, but it'll make the token button not show a hand cursor on hover.

🔧 Fix
 const styles = {
   base: 'w-full py-2 flex items-center justify-between transition-all rounded-xl px-1.5 border duration-150 border-gray-400/25 shadow-sm group',
-  enabled: 'hover:bg-gray-50 cursor pointer',
+  enabled: 'hover:bg-gray-50 cursor-pointer',
   disabled: 'cursor-not-allowed opacity-60',
 };

Comment on lines +1 to +9
import { ChevronIcon } from '@hyperlane-xyz/widgets';
import { SearchInput } from '../../components/input/SearchInput';
import { ChainList } from './ChainList';

interface ChainFilterPanelProps {
searchQuery: string;
onSearchChange: (s: string) => void;
selectedChain: ChainName | null;
onSelectChain: (chain: ChainName | null) => void;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

ChainName type is used but never imported — this won't compile, donkey.

Line 8 references ChainName in the selectedChain prop type, but there's no import for it. This'll blow up at build time faster than an onion in a volcano.

🐛 Proposed fix
-import { ChevronIcon } from '@hyperlane-xyz/widgets';
+import { ChainName } from '@hyperlane-xyz/sdk';
+import { ChevronIcon } from '@hyperlane-xyz/widgets';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { ChevronIcon } from '@hyperlane-xyz/widgets';
import { SearchInput } from '../../components/input/SearchInput';
import { ChainList } from './ChainList';
interface ChainFilterPanelProps {
searchQuery: string;
onSearchChange: (s: string) => void;
selectedChain: ChainName | null;
onSelectChain: (chain: ChainName | null) => void;
import { ChainName } from '@hyperlane-xyz/sdk';
import { ChevronIcon } from '@hyperlane-xyz/widgets';
import { SearchInput } from '../../components/input/SearchInput';
import { ChainList } from './ChainList';
interface ChainFilterPanelProps {
searchQuery: string;
onSearchChange: (s: string) => void;
selectedChain: ChainName | null;
onSelectChain: (chain: ChainName | null) => void;
🤖 Prompt for AI Agents
In `@src/features/chains/ChainFilterPanel.tsx` around lines 1 - 9, The prop type
ChainName used in the ChainFilterPanelProps (for selectedChain and
onSelectChain) is not imported; add the missing named import for ChainName (e.g.
import { ChainName } from '<module where ChainName is defined>') at the top of
ChainFilterPanel.tsx so the ChainFilterPanelProps type compiles; ensure the
import is a named import and matches the module that exports ChainName.

Comment on lines +16 to +17
const { data: icaAddress = null, isLoading, isError, refetch } = useQuery({
queryKey: ['icaAddress', icaApp, userAddress, originChainName, destinationChainName] as const,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Putting an SDK object instance in the queryKey is like tossing a donkey into a pond — things get unstable fast.

icaApp is a class instance; React Query uses deep structural comparison for keys, but complex objects with methods/prototypes won't serialize or compare reliably. This can cause the query to re-fire on every render if the icaApp reference isn't stable (which it likely isn't if created in a hook).

Use a stable primitive identifier instead — for instance, a boolean or a hash of the config that produced it.

🧅 Proposed fix
   const { data: icaAddress = null, isLoading, isError, refetch } = useQuery({
-    queryKey: ['icaAddress', icaApp, userAddress, originChainName, destinationChainName] as const,
+    queryKey: ['icaAddress', userAddress, originChainName, destinationChainName] as const,
     queryFn: async (): Promise<string | null> => {

The enabled guard already prevents execution when icaApp is null, so it doesn't need to be in the key for correctness.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data: icaAddress = null, isLoading, isError, refetch } = useQuery({
queryKey: ['icaAddress', icaApp, userAddress, originChainName, destinationChainName] as const,
const { data: icaAddress = null, isLoading, isError, refetch } = useQuery({
queryKey: ['icaAddress', userAddress, originChainName, destinationChainName] as const,
🤖 Prompt for AI Agents
In `@src/features/swap/hooks/useIcaAddress.ts` around lines 16 - 17, The queryKey
for useQuery currently includes the class instance icaApp which is unstable;
remove icaApp from the queryKey and use a stable primitive instead (e.g., a
unique id/hash from the object or simply omit it and rely on the existing
enabled guard). Update the useQuery call that builds queryKey=['icaAddress',
icaApp, userAddress, originChainName, destinationChainName] to exclude icaApp or
replace it with icaApp?.id or a deterministic configHash, and ensure the enabled
option uses Boolean(icaApp) so the query only runs when the instance is
available; keep function names like useIcaAddress and useQuery unchanged.

Comment on lines +18 to +25
try {
// InterchainAccount requires EVM MultiProvider, not MultiProtocolProvider.
// Build one from the same chain metadata the store already holds.
const evmMultiProvider = new MultiProvider(multiProtocolProvider.metadata);
return InterchainAccount.fromAddressesMap(addressesMap, evmMultiProvider);
} catch {
return null;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Silent catch swallows errors — at least log it, will ya?

Right now if InterchainAccount.fromAddressesMap throws, you get null back with zero indication of what went wrong. That's the kind of thing that'll have you wandering the swamp for hours debugging.

🔧 Proposed fix
     try {
       // InterchainAccount requires EVM MultiProvider, not MultiProtocolProvider.
       // Build one from the same chain metadata the store already holds.
       const evmMultiProvider = new MultiProvider(multiProtocolProvider.metadata);
       return InterchainAccount.fromAddressesMap(addressesMap, evmMultiProvider);
-    } catch {
+    } catch (error) {
+      console.error('Failed to create InterchainAccount app', error);
       return null;
     }

As per coding guidelines, "For unexpected issues (invalid state, broken invariants): fail loudly with throw or console.error. NEVER add silent fallbacks for unexpected issues."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
// InterchainAccount requires EVM MultiProvider, not MultiProtocolProvider.
// Build one from the same chain metadata the store already holds.
const evmMultiProvider = new MultiProvider(multiProtocolProvider.metadata);
return InterchainAccount.fromAddressesMap(addressesMap, evmMultiProvider);
} catch {
return null;
}
try {
// InterchainAccount requires EVM MultiProvider, not MultiProtocolProvider.
// Build one from the same chain metadata the store already holds.
const evmMultiProvider = new MultiProvider(multiProtocolProvider.metadata);
return InterchainAccount.fromAddressesMap(addressesMap, evmMultiProvider);
} catch (error) {
console.error('Failed to create InterchainAccount app', error);
return null;
}
🤖 Prompt for AI Agents
In `@src/features/swap/hooks/useInterchainAccount.ts` around lines 18 - 25, The
catch is silently swallowing errors in useInterchainAccount when
InterchainAccount.fromAddressesMap fails; update the catch to log the error
(including context like addressesMap and multiProtocolProvider.metadata) via
console.error or process logger and then rethrow (or return null only for
expected/handled cases), referencing the InterchainAccount.fromAddressesMap call
and the MultiProvider construction (new
MultiProvider(multiProtocolProvider.metadata)) so failures are visible for
debugging.

Comment on lines 114 to +123
const validate = async (values: TransferFormValues) => {
const routeType = getRouteType(warpCore, tokens, collateralGroups, values);

// Skip full warp validation for swap-bridge routes
if (routeType === 'swap-bridge') {
if (!values.amount || parseFloat(values.amount) <= 0) {
return { amount: 'Invalid amount' };
}
return null;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Swap-bridge validation skips recipient check — this could let users submit with no recipient.

Right now, for swap-bridge routes, the only validation is that amount is positive. There's no check that a recipient (or at minimum a connected wallet on the destination chain) exists. If icaAddress derivation fails and the user hasn't entered a manual recipient, this could lead to a transaction with an empty or undefined recipient downstream.

At least validate that the connected destination wallet or ICA address is available before allowing submission.

🤖 Prompt for AI Agents
In `@src/features/transfer/TransferTokenForm.tsx` around lines 114 - 123, In
validate (used with TransferFormValues) after you detect routeType ===
'swap-bridge', in addition to checking values.amount ensure there is a valid
recipient by verifying either values.recipient is present/non-empty or the
derived ICA/destination wallet is available (the logic that derives ICA address
or checks connected destination wallet should be consulted here); if neither
exists return a validation error (e.g. { recipient: 'Recipient required' }) so
the form cannot submit with an empty/undefined recipient. Use getRouteType,
values.amount and values.recipient/ICA-derivation check to locate where to add
this check.

Comment on lines +1018 to 1043
function getRouteType(
_warpCore: WarpCore,
tokens: Token[],
collateralGroups: Map<string, Token[]>,
values: TransferFormValues,
): TransferRouteType {
const originToken = getTokenByKey(tokens, values.originTokenKey);
const destinationToken = getTokenByKey(tokens, values.destinationTokenKey);
if (!originToken || !destinationToken) return 'unavailable';
if (checkTokenHasRoute(originToken, destinationToken, collateralGroups)) return 'warp';
if (isSwapSupported(originToken.chainName, destinationToken.chainName)) {
const destinationTokenAddress =
getSwappableAddress(destinationToken) ?? destinationToken.addressOrDenom;
if (
isDemoSwapBridgePath({
originChainName: originToken.chainName,
destinationChainName: destinationToken.chainName,
destinationTokenAddress,
destinationRouteAddress: destinationToken.addressOrDenom,
})
) {
return 'swap-bridge';
}
}
return 'unavailable';
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

getRouteType duplicates useTransferRoute logic.

This standalone function replicates the same route-classification logic that useTransferRoute (from ./useTransferRoute) already implements. According to the external snippet, useTransferRoute does the same origin/destination token checks, checkTokenHasRoute, isSwapSupported, and isDemoSwapBridgePath calls.

Having two copies means any future route-type change needs updating in both places. Consider extracting the pure logic into a shared function that both useTransferRoute and getRouteType (used in validate) can call.

#!/bin/bash
# Verify that useTransferRoute has the same logic as getRouteType
ast-grep --pattern 'export function useTransferRoute($$$) {
  $$$
}'
🤖 Prompt for AI Agents
In `@src/features/transfer/TransferTokenForm.tsx` around lines 1018 - 1043,
getRouteType duplicates the route-classification logic in useTransferRoute;
extract the pure decision logic into a single shared helper (e.g.,
determineTransferRoute or computeRouteType) that accepts the same inputs
(tokens, collateralGroups, values or the minimal inputs: originToken,
destinationToken, collateralGroups) and returns TransferRouteType, then update
both useTransferRoute and getRouteType to call that helper; ensure the helper
uses the same calls already present (getTokenByKey, checkTokenHasRoute,
isSwapSupported, getSwappableAddress, isDemoSwapBridgePath) and keep public
signatures compatible with TransferFormValues/TransferRouteType so callers need
only replace direct logic with a single call to the new function.

Comment on lines +17 to +19
PostingCommitment = 'posting-commitment',
SigningSwapBridge = 'signing-swap-bridge',
ConfirmingSwapBridge = 'confirming-swap-bridge',
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n 'SentTransferStatuses|FinalTransferStatuses' --type=ts --type=tsx -C3

Repository: hyperlane-xyz/hyperlane-warp-ui-template

Length of output: 111


🏁 Script executed:

# Also check if the new statuses are referenced elsewhere
rg -n 'PostingCommitment|SigningSwapBridge|ConfirmingSwapBridge' --type=ts --type=tsx -C2

Repository: hyperlane-xyz/hyperlane-warp-ui-template

Length of output: 111


🏁 Script executed:

# Look at the types.ts file to see the full status definitions
fd -n 'types.ts' src/features/transfer | head -1 | xargs cat -n

Repository: hyperlane-xyz/hyperlane-warp-ui-template

Length of output: 314


🏁 Script executed:

rg -n 'SentTransferStatuses|FinalTransferStatuses' -C3

Repository: hyperlane-xyz/hyperlane-warp-ui-template

Length of output: 3166


🏁 Script executed:

rg -n 'PostingCommitment|SigningSwapBridge|ConfirmingSwapBridge' -C2

Repository: hyperlane-xyz/hyperlane-warp-ui-template

Length of output: 2501


🏁 Script executed:

cat -n src/features/transfer/types.ts

Repository: hyperlane-xyz/hyperlane-warp-ui-template

Length of output: 1637


🏁 Script executed:

sed -n '6,42p' src/features/transfer/utils.ts

Repository: hyperlane-xyz/hyperlane-warp-ui-template

Length of output: 1912


🏁 Script executed:

# Check if isTransferSent or SentTransferStatuses is used elsewhere for polling/retry logic
rg -n 'isTransferSent' --context=5

Repository: hyperlane-xyz/hyperlane-warp-ui-template

Length of output: 2614


🏁 Script executed:

# Check how isFinal is used in TransfersDetailsModal and whether there's rendering logic affected
sed -n '102,130p' src/features/transfer/TransfersDetailsModal.tsx

Repository: hyperlane-xyz/hyperlane-warp-ui-template

Length of output: 1092


🏁 Script executed:

# Check if there are other uses of these status arrays that might be affected
rg -n 'getIconByTransferStatus' --context=10

Repository: hyperlane-xyz/hyperlane-warp-ui-template

Length of output: 8164


Add status labels and finality classification for new swap-bridge statuses.

The three new statuses are missing from critical downstream handling:

  1. Missing in getTransferStatusLabel() — the UI will show blank ("...") for PostingCommitment, SigningSwapBridge, and ConfirmingSwapBridge.

  2. Not in SentTransferStatuses or FinalTransferStatuses — this breaks multiple flows:

    • In TransfersDetailsModal, isFinal only becomes true for ConfirmedTransfer or Failed. Swap-bridge transfers won't display the completion header (date + status icon).
    • In SideBarMenu, only statuses in STATUSES_WITH_ICON show icons; swap-bridge transfers will show a spinner forever.
    • The wallet reconnection check (line 13 in utils.ts) will show "Please connect wallet" even after the swap-bridge transaction is confirmed on-chain.
    • In store.ts, failUnconfirmedTransfers() won't mark incomplete swap-bridge transfers as failed if the wallet disconnects.

Add appropriate status labels in getTransferStatusLabel() and classify ConfirmingSwapBridge (and possibly PostingCommitment) in either SentTransferStatuses or FinalTransferStatuses, or handle them with dedicated logic.

🤖 Prompt for AI Agents
In `@src/features/transfer/types.ts` around lines 17 - 19, The new enum values
PostingCommitment, SigningSwapBridge, and ConfirmingSwapBridge were added but
not handled downstream; update getTransferStatusLabel() to return human-readable
labels for those three values, and add the new statuses to the appropriate
status sets (either SentTransferStatuses and/or FinalTransferStatuses) so
finality logic recognizes swap-bridge flows—specifically ensure
ConfirmingSwapBridge is treated as final (or Sent if you have a separate
in-flight concept) so TransfersDetailsModal.isFinal, STATUSES_WITH_ICON (used by
SideBarMenu), the wallet reconnection check in utils.ts, and
failUnconfirmedTransfers() in store.ts behave correctly; if you decide
PostingCommitment should remain in-flight, include it in SentTransferStatuses
but not FinalTransferStatuses, and ensure SigningSwapBridge is classified
consistently with its lifecycle.

Comment on lines +107 to +135
async function checkAndApprove(
walletClient: WalletClient,
publicClient: any,
tokenAddress: Address,
spender: Address,
amount: bigint,
): Promise<void> {
const owner = walletClient.account?.address;
if (!owner) throw new Error('Wallet not connected');

const currentAllowance = await publicClient.readContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'allowance',
args: [owner, spender],
});

if (currentAllowance < amount) {
const approveHash = await walletClient.writeContract({
account: owner,
address: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [spender, maxUint256],
chain: null,
});
await publicClient.waitForTransactionReceipt({ hash: approveHash, confirmations: 1 });
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

publicClient: any loses all type safety — get that outta me swamp.

The publicClient parameter on line 109 is typed as any, which means readContract and waitForTransactionReceipt calls have no compile-time checking. Since you're creating the client via createPublicClient from viem, you can use the proper return type.

Proposed fix
+import { type PublicClient } from 'viem';
+
 async function checkAndApprove(
   walletClient: WalletClient,
-  publicClient: any,
+  publicClient: PublicClient,
   tokenAddress: Address,
   spender: Address,
   amount: bigint,
 ): Promise<void> {
🤖 Prompt for AI Agents
In `@src/features/transfer/useSwapBridgeTransfer.ts` around lines 107 - 135, The
parameter publicClient in checkAndApprove is typed as any, losing type safety
for publicClient.readContract and publicClient.waitForTransactionReceipt; change
the publicClient parameter type to viem's PublicClient (import { PublicClient }
from 'viem' or your project's viem wrapper) and update the function signature
async function checkAndApprove(walletClient: WalletClient, publicClient:
PublicClient, tokenAddress: Address, spender: Address, amount: bigint):
Promise<void>, leaving calls to publicClient.readContract and
publicClient.waitForTransactionReceipt as-is so TypeScript can validate their
return types and args (ensure createPublicClient is used where the client is
created so the type lines up).

Comment on lines +393 to +414
onStatusChange(TransferStatus.PostingCommitment);
try {
const payload = buildPostCallsPayload({
calls: commitmentPayload.normalizedCalls,
relayers: [],
salt,
commitmentDispatchTx: hash,
originDomain: originConfig.domainId,
});
const relayerResponse = await shareCallsWithPrivateRelayer(
COMMITMENTS_SERVICE_URL,
payload,
);

if (!relayerResponse.ok) {
throw new Error('Relayer rejected commitment payload.');
}
} catch (error) {
throw new Error(
`Failed to post ICA call commitment to relayer service. Without this, reveal cannot execute destination calls. Root cause: ${toErrorMessage(error)}`,
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

No timeout on relayer service call — could hang indefinitely.

The shareCallsWithPrivateRelayer call has no visible timeout. If the relayer service is down or unresponsive, this would leave the user stuck at the "Posting Commitment" state with no way to recover. The transaction is already on-chain at this point, so a hung post-commitment call means the user has paid fees but the reveal can't execute.

Consider adding a timeout or at minimum surfacing a more specific recovery message.

🤖 Prompt for AI Agents
In `@src/features/transfer/useSwapBridgeTransfer.ts` around lines 393 - 414, The
call to shareCallsWithPrivateRelayer in the
onStatusChange(TransferStatus.PostingCommitment) block can hang indefinitely;
modify this call to enforce a timeout and surface a clear recovery error: create
an AbortController (or use a timeout wrapper) and pass its signal into
shareCallsWithPrivateRelayer if the function supports it, or wrap the promise in
a Promise.race with a timeout that aborts/throws after a configurable duration
(e.g., 10–30s); on timeout, throw a descriptive error that includes context
(commitment hash, originDomain via buildPostCallsPayload and
COMMITMENTS_SERVICE_URL) and use toErrorMessage for consistency so callers can
present a specific recovery message instead of hanging.

paulbalaji and others added 2 commits February 12, 2026 21:49
Removed duplicate getIcaCommitRevealFee implementation that duplicated
the SDK's getIcaFee. All call sites now import directly from @hyperlane-xyz/sdk.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extracted COMMITMENTS_SERVICE_URL, randomSalt, and toErrorMessage into
src/features/swap/utils.ts. Both useIcaTransaction and useSwapBridgeTransfer
now import from the shared module instead of defining their own copies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Consolidated split getIcaFee import into single SDK import block.
Prettier auto-formatted import ordering in affected files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

3 participants