do not merge: Nambrot/crossstableswap demo#884
Conversation
chore: change token identification to use an identifier instead of tokenIndex
## Summary Extracts generic UI fixes from #877 that can be applied to the rebranding branch: - **Remove rounded corners**: Removed rounded-t-md from wallet sidebar header for cleaner look - **Smaller token selectors**: Reduced size of send/receive token selector buttons for a more compact UI - **Page centralization**: Vertically center the transfer form with responsive tip card layout - **Warning banner styling**: Added rounded corners, improved spacing and alignment with tip card - **Favicon & app icons**: Updated all favicon and app icon files with new branding ## Test plan - [ ] Check sidebar header has no rounded corners - [ ] Confirm token selectors are visually smaller - [ ] Test page layout is vertically centered on various screen sizes - [ ] Check warning banner has rounded corners and proper spacing from tip card - [ ] Verify new favicons and app icons display correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- greptile_comment --> <h3>Greptile Summary</h3> This PR extracts clean UI refinements from #877, focusing on making the interface more compact and polished. The changes reduce padding throughout the transfer form (from `p-4` to `p-3`), shrink token selector icons (44px → 36px), and tighten spacing across components. The page layout now properly centers vertically with `items-center` on the container, and the warning banner gained rounded corners for visual consistency. **Key improvements:** - Compact token selectors with smaller icons and tighter spacing - Vertically centered transfer form improves visual balance - Warning banner styling matches tip card with rounded corners - Sidebar header cleaned up (removed `rounded-t-md`) - Escape key closes sidebar (proper cleanup prevents memory leaks) - Responsive tip card layout with better positioning The Escape key implementation is well done - it properly cleans up the event listener and only attaches when the sidebar is open. All spacing adjustments are consistent and maintain proper visual hierarchy. <details><summary><h3>Confidence Score: 5/5</h3></summary> - This PR is safe to merge with no risk - All changes are purely visual styling adjustments (Tailwind classes) with no logic modifications. The Escape key handler is properly implemented with cleanup. No security concerns or breaking changes. - No files require special attention </details> <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | src/components/layout/AppLayout.tsx | Vertically centered main content area by adding `items-center` and changing `mt-4` to `my-4` | | src/features/transfer/TransferTokenForm.tsx | Reduced spacing throughout form: smaller gaps, tighter padding, smaller font sizes for compact view | | src/features/wallet/SideBarMenu.tsx | Removed `rounded-t-md` from header and added Escape key listener for closing sidebar | | src/pages/index.tsx | Restructured layout for responsive tip card positioning with cleaner flex/gap layout | </details> </details> <details><summary><h3>Sequence Diagram</h3></summary> ```mermaid sequenceDiagram participant User participant Page as index.tsx participant Layout as AppLayout participant Form as TransferTokenForm participant TokenSelect as TokenSelectField participant Sidebar as SideBarMenu participant Banner as WarningBanner User->>Page: Visits app Page->>Layout: Renders with centered layout Layout->>Form: Displays transfer form (vertically centered) Layout->>Banner: Shows warning banner (with rounded corners) User->>TokenSelect: Clicks token selector TokenSelect->>TokenSelect: Opens modal (compact 36px icons) User->>Sidebar: Opens wallet sidebar Sidebar->>Sidebar: Displays without rounded header User->>Sidebar: Presses Escape key Sidebar->>Sidebar: Closes sidebar User->>Form: Enters amount in compact input Form->>Form: Validates with tighter spacing (p-3) User->>Form: Clicks swap button (8x8px) Form->>Form: Swaps origin/destination ``` </details> <!-- greptile_other_comments_section --> <!-- /greptile_comment --> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…selector feat: Unified Chain Token Selector Modal
feat: UI complete rebrand
- Add isStableSwapRoute() helper to detect stableswap token pairs - Pass destinationToken to WarpCore for stableswap transfers - Pass destinationToken for stableswap fee estimation - Set warpRouteWhitelist to empty to use local config
|
The latest updates on your projects. Learn more about Vercel for GitHub.
5 Skipped Deployments
|
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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. 📝 WalkthroughWalkthroughMigrates token handling from index-based to token-key identity, adds unified token/chain selection UI and route-aware token/collateral utilities, refactors transfer/fee/max logic for route selection, adds many new icons/components, overhauls styling/tailwind, and updates config/constants and store to expose tokens and collateralGroups. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant TokenField as TokenSelectField
participant Modal as UnifiedTokenChainModal
participant ChainPanel as ChainFilterPanel
participant TokenPanel as TokenListPanel
User->>TokenField: open selector
TokenField->>Modal: openModal()
Modal->>ChainPanel: init callbacks
Modal->>TokenPanel: init callbacks
alt user filters chains
User->>ChainPanel: select chain
ChainPanel->>Modal: selectedChain
Modal->>TokenPanel: apply chainFilter
end
alt user searches tokens
User->>TokenPanel: type query
TokenPanel->>TokenPanel: filter & sort (uses collateralGroups)
TokenPanel->>User: display results
end
User->>TokenPanel: choose token
TokenPanel->>Modal: onSelect(token)
Modal->>TokenField: return token key
TokenField->>User: update form originTokenKey/destinationTokenKey
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 17
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 (5)
src/features/chains/ChainConnectionWarning.tsx (1)
13-14: MissingChainNametype import, mate.The
ChainNametype is used in the component props but it's not imported anywhere in this file. This'll give ye a TypeScript error faster than you can say "get out of me swamp."🐛 Proposed fix
-import { ChainMetadata, isRpcHealthy } from '@hyperlane-xyz/sdk'; +import { ChainMetadata, ChainName, isRpcHealthy } from '@hyperlane-xyz/sdk';src/features/transfer/useTokenTransfer.ts (1)
268-280: Potential undefined variable in error path.If an error is thrown before
originis assigned (e.g., at line 121 or 133), the error handler at line 276 will reference an undefinedoriginvariable. This could cause a secondary error or confusing message.🐛 Suggested fix
} else if ( errorDetails.includes(TRANSFER_TIMEOUT_ERROR1) || errorDetails.includes(TRANSFER_TIMEOUT_ERROR2) ) { + const chainName = origin ?? 'the origin chain'; toast.error( - `Transaction timed out, ${getChainDisplayName(multiProvider, origin)} may be busy. Please try again.`, + `Transaction timed out, ${origin ? getChainDisplayName(multiProvider, origin) : 'the origin chain'} may be busy. Please try again.`, ); }src/features/tokens/balances.ts (1)
13-13: Add missing type imports forChainNameandAddress.The
useBalancefunction signature usesChainNameandAddresstypes (line 13), but they're not imported. These types need to be brought in, likely from@hyperlane-xyz/sdkwhereITokenandMultiProtocolProvidercome from.src/features/transfer/TransfersDetailsModal.tsx (1)
48-60:TransferPropertyvalue typing/guards look unsafe withtransfer || {}.
If any ofsender/recipient/msgId/...are missing,CopyButtonand rendering can get fedundefinedwhileTransferPropertyclaimsvalue: string. Better to guard at the callsite or widen the prop type.Possible fix (widen + guard)
-function TransferProperty({ name, value, url }: { name: string; value: string; url?: string }) { +function TransferProperty({ + name, + value, + url, +}: { + name: string; + value?: string; + url?: string; +}) { return ( <div> <div className="flex items-center justify-between"> <label className="text-xs leading-normal tracking-wider text-gray-350">{name}</label> <div className="flex items-center space-x-2"> {url && ( <a href={url} target="_blank" rel="noopener noreferrer"> <Image src={LinkIcon} width={14} height={14} alt="" /> </a> )} - <CopyButton copyValue={value} width={14} height={14} className="opacity-40" /> + <CopyButton copyValue={value ?? ''} width={14} height={14} className="opacity-40" /> </div> </div> <div className="mt-1 truncate text-xs leading-normal tracking-wider text-gray-900"> - {value} + {value ?? ''} </div> </div> ); }Also applies to: 167-196, 247-266
src/features/transfer/useFeeQuotes.ts (1)
47-55: Query key includes an unstable Promise object—this'll cause cache misses and refetches.The
senderPubKeydestructured fromgetAccountAddressAndPubKey()is typed asPromise<HexString>(see line 81), not a stable string. Throwing a Promise directly into the query key means it changes identity on every render, so React Query can't recognize the cached result and refetches unnecessarily each time.Either await the Promise before adding it to the key, derive a stable identifier from it, or remove it from the key entirely if you're already passing it to
queryFn.
🤖 Fix all issues with AI agents
In @package.json:
- Around line 24-26: The package.json currently pins @hyperlane-xyz/sdk and
@hyperlane-xyz/utils to pre-release stableswap builds with commit hashes and
leaves @hyperlane-xyz/widgets at 20.1.0; revert those two entries to the
published stable versions (remove the "-stableswap.*" pins and commit hashes)
and align all three @hyperlane-xyz/* packages to the same published version
(e.g., set @hyperlane-xyz/sdk, @hyperlane-xyz/utils, and @hyperlane-xyz/widgets
to the current stable release) so no pre-release or commit-hash pins land on
main.
In @src/components/input/TextField.tsx:
- Around line 32-33: The default TextField styling removed padding and text-size
which leaves callers like SelectOrInputTokenIds (className="w-full"),
TransferTokenForm (uses text-xl but no padding), and SearchInput (uses invalid
"all:py-2") rendering cramped; restore sensible defaults by updating the
defaultClassName constant in TextField.tsx to include padding (e.g., px-3 py-2)
and a base text-size (e.g., text-sm or text-base), and then fix usages: either
remove redundant sizing/padding from callers or add explicit padding/text
classes where needed (update TransferTokenForm to add px/py if it wants
text-xl), and replace the invalid "all:py-2" in SearchInput with a valid
Tailwind class like "py-2".
In @src/consts/args.ts:
- Around line 3-8: The change removed the legacy single "token" query parameter
and breaks old deep links; update the queryParams parsing logic to detect a
legacy "token" param and map it into the new keys
(WARP_QUERY_PARAMS.ORIGIN_TOKEN and WARP_QUERY_PARAMS.DESTINATION_TOKEN) when
those new params are not present; modify the queryParams utility to prefer
explicit originToken/destinationToken but fall back to populating both from
"token" for backward compatibility, and update related tests to cover legacy and
new-param behaviors.
In @src/consts/warpRouteWhitelist.ts:
- Line 5: The warpRouteWhitelist constant was changed to an empty array which is
truthy and causes warpCoreConfig.ts to call filterToIds() and yield zero routes;
revert warpRouteWhitelist back to null to mean "include everything" or replace
the empty array with explicit route IDs from warpRoutes.yaml (e.g., the
Arbitrum/Base USDC/USDT and Soneium IDs) so filterToIds() returns the intended
subset; locate the warpRouteWhitelist export and update its value accordingly
and verify behavior in warpCoreConfig.ts and any calls to filterToIds().
In @src/features/chains/ChainFilterPanel.tsx:
- Around line 5-9: The interface ChainFilterPanelProps references the type
ChainName but the file is missing its import; add an import for the ChainName
type (the same exported symbol used elsewhere in the project) at the top of the
file so TypeScript recognizes ChainName and the props signature for
ChainFilterPanelProps compiles correctly.
In @src/features/chains/ChainList.tsx:
- Around line 5-9: The ChainListProps interface in ChainList.tsx references the
ChainName type but there is no import; add an import for the ChainName type at
the top of ChainList.tsx (the same module/source used elsewhere in the repo
where ChainName is defined) so the interface compiles and type-checks; ensure
the import uses a type-only import if your bundler/TS config prefers (e.g.,
`import type { ChainName } from '...'`) and keep the symbol name ChainName to
match the interface.
In @src/features/tokens/hooks.ts:
- Around line 23-30: The parsing in findTokenByChainSymbol using
chainSymbol.split('-') is brittle if chainName can contain dashes; change it to
locate the last dash (e.g., use lastIndexOf('-')) and split into chainName =
substring(0, lastDash) and symbol = substring(lastDash + 1), validate lastDash
>= 0 and both parts non-empty, and apply the same fix to the other occurrence
around the function/lines referenced (the second parser at lines ~68-71) so both
places safely handle chainNames containing dashes.
In @src/features/tokens/TokenList.tsx:
- Around line 10-16: TokenListProps uses the ChainName type but it isn't
imported, which will break TypeScript compilation; add an import for ChainName
at the top of TokenList.tsx alongside the existing type imports (e.g., where
Token and TokenSelectionMode are imported) so TokenListProps: interface
TokenListProps { ... chainFilter: ChainName | null; ... } resolves correctly.
- Around line 58-77: The search filter in TokenList's useMemo assumes token
fields like t.name and t.symbol exist and calls .toLowerCase() on them, which
can throw for missing values; update the filtered predicate to safely coerce
each possibly-missing field before calling toLowerCase() (e.g., use (t.name ||
'').toLowerCase(), (t.symbol || '').toLowerCase(), (t.addressOrDenom ||
'').toLowerCase(), (t.collateralAddressOrDenom || '').toLowerCase()), and ensure
chainDisplayName is also coerced (e.g., (getChainDisplayName(... ) ||
'').toLowerCase()) so the filter never invokes .toLowerCase() on undefined.
In @src/features/tokens/TokenListPanel.tsx:
- Around line 44-50: The TokenListPanel usage passes an unsupported aria-label
prop to SearchInput causing a TypeScript error; either remove the aria-label
from TokenListPanel's SearchInput invocation or update the SearchInput component
to accept an optional ariaLabel prop and forward it to the underlying <input>
(e.g., add ariaLabel?: string to SearchInput's props interface in
SearchInput.tsx and spread it as aria-label on the input element), then update
TokenListPanel to use ariaLabel if you choose the proper fix.
In @src/features/tokens/utils.ts:
- Around line 332-345: The current logic uses a fallback return of
routeTokens[0] which can return an unrelated token and the symbol-only match is
ambiguous; update the matcher to: (1) only match by collateral when
normalizedOriginCollateral and normalizedRouteCollateral exist; (2) only fall
back to symbol matching if the symbol is unambiguous (e.g., routeTokens.filter(t
=> t.symbol === originToken.symbol).length === 1); and (3) remove the final
fallback of "return matchingToken || routeTokens[0]" so the function returns
matchingToken (which may be undefined) and lets the caller handle no-match
cases. Ensure you touch the matchingToken find callback, the symbol-only branch,
and replace the final return to return matchingToken.
In @src/features/transfer/RecipientConfirmationModal.tsx:
- Around line 18-30: Guard the chainName before calling
getAccountAddressAndPubKey and trim the recipient value: first compute a trimmed
recipient (e.g., values.recipient?.trim()) and use that for truthiness checks,
and only call getAccountAddressAndPubKey when destinationToken and
destinationToken.chainName are present (otherwise set connectedDestAddress to
undefined/empty). Then set recipient = trimmedRecipient || connectedDestAddress
|| ''. Reference getAccountAddressAndPubKey, destinationToken, values.recipient,
connectedDestAddress and recipient when making the change.
In @src/features/transfer/TransferFeeModal.tsx:
- Around line 19-21: The Tailwind class "max-w-128" used on the Modal component
(Modal isOpen={isOpen} ... panelClassname="p-0 max-w-sm md:max-w-128
overflow-hidden") isn't defined in your tailwind config so it won't be
generated; open your tailwind.config.js and add a 128 entry under
theme.extend.maxWidth (e.g., map 128 to '32rem' or your desired value) so the
"md:max-w-128" utility exists and the modal width will behave as expected.
In @src/features/transfer/TransferTokenForm.tsx:
- Around line 148-155: The effect currently uses initialValues.originTokenKey so
it only runs for the initial selection; change it to read the live Formik value
instead (e.g. use useFormikContext() or receive values/originTokenKey prop) so
it reacts to user changes: inside the useEffect that calls getTokenByKey(tokens,
...), replace initialValues.originTokenKey with the current Formik
values.originTokenKey and keep setOriginChainName(originToken.chainName) as
before so the origin chain store updates whenever the user picks a different
origin token.
- Around line 378-412: The MaxButton currently passes values.recipient directly
to fetchMaxAmount which breaks when the UI relies on the connected destination
wallet default; compute the same "effective recipient" used by fees/validation
and pass that to fetchMaxAmount instead of values.recipient—i.e., derive the
fallback recipient from the connected accounts/selected destination token (same
logic used elsewhere in the form) and use that value in the fetchMaxAmount call
inside MaxButton before formatting and setFieldValue('amount', ...).
In @src/features/wallet/RecipientAddressModal.tsx:
- Around line 1-5: The file uses React.ChangeEvent types but doesn't import
React types; update the imports at the top of RecipientAddressModal (where
isValidAddress/ProtocolType/Modal/XIcon/useState/SolidButton are imported) to
include the type import (e.g., import type { ChangeEvent } from 'react' or
import React and use React.ChangeEvent) so the type references in the component
(e.g., the onChange/handleChange handlers around the RecipientAddressModal form,
including the code referenced in lines ~44-47) resolve and the module compiles.
In @src/styles/globals.css:
- Around line 5-27: The @font-face rules in globals.css reference
'/fonts/PPValve-PlainVariable.woff2' and '/fonts/PPFraktionMono-Variable.woff2'
but those files and the public/fonts directory are missing; fix by either adding
the actual .woff2 files to the public/fonts directory and committing them, or
update the src URLs in the 'PP Valve' and 'PP Fraktion Mono' @font-face
declarations to the correct existing asset paths (or a CDN) and verify the files
are served in production; ensure font filenames in the CSS exactly match the
committed asset names and that you keep font-display: swap.
🟡 Minor comments (10)
src/features/transfer/FeeSectionButton.tsx-26-32 (1)
26-32: Path fill colors don't match the text color states.Look, the text goes from
gray-500togray-600on hover, which makes sense. But the[&_path]:fill-gray-600stays the same in both states - it's gray-600 all the time. Meanwhile, you're passingColor.gray[500]to those icon components, but that CSS selector just tramples right over it like a donkey through a field.If the icons are meant to follow the text color, the path fills should be gray-500 normally and gray-600 on hover:
🔧 Suggested fix
<button - className="flex w-fit items-center font-secondary text-xxs text-gray-500 hover:text-gray-600 [&_path]:fill-gray-600 [&_path]:hover:fill-gray-600" + className="flex w-fit items-center font-secondary text-xxs text-gray-500 hover:text-gray-600 [&_path]:fill-gray-500 [&_path]:hover:fill-gray-600" type="button" onClick={open} >src/components/buttons/SolidButton.tsx-33-41 (1)
33-41: Add explicit text color to gradient button variantsThe gradients look layered nicely, but there's a gap: the
accent-gradientanderror-gradientvariants are missing explicit text color. Looking at the Tailwind config, those gradient backgrounds are purely background-image definitions—no text styling baked in. Every other variant setstext-whiteor similar, but accent and red don't. That means text color just inherits from the browser defaults, which could get murky against those gradient backgrounds.Add
text-white(or whatever works with yer design) to lines 34 and 40 to match the pattern of other variants. Like onions, buttons have layers—backgrounds, text, shadows—and they're all gotta work together.src/components/input/SearchInput.tsx-29-29 (1)
29-29: Remove the redundantall:responsive breakpoint prefix from base styles.The
allbreakpoint is configured in the Tailwind config asscreens: { all: '1px' }, meaning it applies at 1px and above—basically always. Usingall:border-gray-300is like putting the same thing twice in your porridge; it works, but there's no point. Just useborder-gray-300 py-2 text-sm focus:border-blue-400without theall:prefix, since these are base styles that should apply everywhere anyway.src/features/transfer/maxAmount.ts-50-55 (1)
50-55: Consider adding error feedback whenfetchMaxAmountreturnsundefined.Callers do handle the undefined return via the
isNullishcheck, but it silently returns without user-facing feedback. When either the route lookup or destination token fails, the click handler exits without informing the user why the max amount couldn't be calculated—they'll just see the button appear unchanged. Adding a toast notification or error message would make this clearer rather than leaving it as a silent no-op.src/features/tokens/TokenSelectField.tsx-159-163 (1)
159-163: Fix the CSS class typo - missing hyphen in cursor-pointer.There's a wee typo in your styles object that'll keep the cursor from changing on hover.
🔧 Proposed fix
const styles = { base: 'w-full py-2 flex items-center justify-between transition-all rounded-xl px-1.5', - enabled: 'hover:bg-gray-50 cursor pointer', + enabled: 'hover:bg-gray-50 cursor-pointer', disabled: 'cursor-not-allowed opacity-60', };.gitignore-40-40 (1)
40-40: Consider documenting where to obtain the custom fonts.The fonts are actually optional—the CSS already includes system font fallbacks (
['PP Fraktion Mono', 'system-ui', 'sans-serif']and['PP Valve', 'system-ui', 'sans-serif']), andfont-display: swaphandles graceful degradation. So ignoring/public/fontswon't break anything for new developers.That said, there's a documentation gap. The CSS comment tells folks where to put the fonts (
public/fonts/), but doesn't explain where to get them in the first place. If you want developers to use these custom fonts, add a note to the README or CUSTOMIZE.md about how to obtain PP Fraktion Mono and PP Valve (whether that's purchasing, downloading, or generating them).src/features/chains/ChainFilterPanel.tsx-36-41 (1)
36-41: Remove unsupportedaria-labelprop from SearchInput.The SearchInput component only accepts
inputRef,value,onChange, andplaceholderprops. Thearia-labelattribute won't be passed through to the underlying input element. Either add support foraria-labelin SearchInput or remove it from the usage here.src/components/nav/Nav.tsx-48-60 (1)
48-60: Addrel="noopener noreferrer"for external links, will ye?When ye open links in new tabs with
target="_blank", the new page can access the opener window throughwindow.opener. Addingrel="noopener noreferrer"prevents this - it's a wee security thing that keeps everything in its proper place.🔒 Proposed fix
<Link ref={ref} className={clsx( 'flex items-center gap-2 text-primary-500 decoration-primary-500 underline-offset-2 hover:underline', className, )} target="_blank" + rel="noopener noreferrer" href={item.url} >src/features/transfer/TransferTokenForm.tsx-894-899 (1)
894-899: Validation error field key looks wrong.
Invalid recipientis attached toamount.Proposed fix
- if (!recipient) return [{ amount: 'Invalid recipient' }, null]; + if (!recipient) return [{ recipient: 'Invalid recipient' }, null];src/features/tokens/hooks.ts-94-101 (1)
94-101: Destination fallback match is case-sensitive.
This can miss the token if symbol casing differs.Proposed fix
- destinationToken = connectedChain - ? tokens.find((t) => t.chainName === connectedChain && t.symbol === connectedSymbol) + destinationToken = connectedChain + ? tokens.find( + (t) => + t.chainName === connectedChain && + t.symbol.toLowerCase() === (connectedSymbol ?? '').toLowerCase(), + ) : undefined;
🧹 Nitpick comments (23)
src/pages/_app.tsx (1)
36-39: The comment might be outdated now that you've switched to Tailwind font classes.The comment mentions needing font definitions "both here and in _document.tsx" for Next.js to load fonts, but since you've moved from
MAIN_FONT.variableto the static Tailwind classfont-primary, this comment might not be accurate anymore. Either update the comment to reflect the new approach or remove it if it no longer applies.✏️ Consider updating the comment
- // Note, the font definition is required both here and in _document.tsx - // Otherwise Next.js will not load the font + // Apply primary font and base text color to the app root return ( <div className="font-primary text-black">src/components/icons/HyperlaneTransparentLogo.tsx (1)
3-18: Consider accepting props for flexibility.Now look, this logo's got no way to change its size or color from the outside - it's stuck with hardcoded dimensions and a white stroke. Unlike the other icons in your codebase that accept
DefaultIconProps, this one's a bit set in its ways.If you ever need to resize it or use it somewhere with a light background, you'll be in a pickle. Might want to make it a bit more flexible, unless you're certain it'll only ever be used one way.
♻️ Suggested enhancement for prop support
import { memo } from 'react'; +import { DefaultIconProps } from '@hyperlane-xyz/widgets'; -function _HyperlaneTransparentLogo() { +function _HyperlaneTransparentLogo({ color = 'white', ...props }: DefaultIconProps) { return ( <svg width="146" height="127" viewBox="0 0 146 127" fill="none" xmlns="http://www.w3.org/2000/svg" + {...props} > <path d="M111.997 0.5C118.021..." - stroke="white" + stroke={color} /> </svg> ); }src/styles/globals.css (1)
78-96: Consider using theme tokens for scrollbar colors.The hardcoded hex values
#e8caffand#d4a3ffwork fine, but if you ever want to support theming or dark mode, pulling these from CSS variables or the theme config would make life easier down the road. Not a dealbreaker by any means - scrollbar styling can be finicky with variables in some browsers.src/features/transfer/RecipientConfirmationModal.tsx (1)
41-41: Make the recipient address easier to visually verify (wrap/monospace).Addresses can be long; consider
break-all+font-monoso folks don’t miss a sneaky character.Proposed tweak
- <p className="rounded-lg bg-primary-500/5 p-2 text-center text-sm">{recipient}</p> + <p className="break-all rounded-lg bg-primary-500/5 p-2 text-center font-mono text-sm"> + {recipient} + </p>src/features/tokens/TokenListPanel.tsx (2)
10-21: Consider collapsingchainFilter+selectedChaininto a single source of truth.Two “selected chain” props is like onion layers you don’t need—easy for state to drift (desktop sets
chainFilter, mobile setsselectedChain).
8-9: TypepreferredChainsasChainName[]to matchMobileChainQuickSelect.Right now it's typed as
string[], but the component expectsChainName[](from@hyperlane-xyz/sdk). Typing it properly upfront keeps things from gettin' messy with casts down the road.src/features/wallet/RecipientAddressModal.tsx (1)
62-73: Add minimal input a11y wiring (label/aria-invalid/aria-describedby).Small tweak, big payoff for screen readers and form tooling.
Proposed tweak
<div className="px-4 pb-4"> + <label htmlFor="recipient-address" className="sr-only"> + Receive address + </label> <input + id="recipient-address" type="text" value={address} onChange={handleAddressChange} placeholder="Paste Wallet Address" + aria-invalid={!!error} + aria-describedby={error ? 'recipient-address-error' : undefined} className={`w-full rounded-lg border px-4 py-3 text-sm text-gray-700 placeholder:text-gray-400 focus:outline-none ${ error ? 'border-red-500 focus:border-red-500' : 'border-gray-300 focus:border-primary-500' }`} /> - {error && <p className="mt-1 text-sm text-red-500">{error}</p>} + {error && ( + <p id="recipient-address-error" className="mt-1 text-sm text-red-500"> + {error} + </p> + )}src/components/nav/Header.tsx (1)
17-17: Consider adding meaningful alt text for accessibility.The images use empty alt attributes (
alt=""). While this is fine for purely decorative images, the logo and name images likely convey meaning to screen reader users. Consider adding descriptive alt text likealt="App logo"andalt="App name".♿ Suggested accessibility improvement
- <Image src={Logo} width={36} alt="" className="h-auto" /> + <Image src={Logo} width={36} alt="App logo" className="h-auto" />- <Image src={Logo} width={46} alt="" className="h-auto" /> - <Image src={Name} width={150} alt="" className="ml-1.5" /> + <Image src={Logo} width={46} alt="App logo" className="h-auto" /> + <Image src={Name} width={150} alt="App name" className="ml-1.5" />Also applies to: 36-39
src/components/icons/HyperlaneGradientLogo.tsx (1)
4-28: Nice and tidy icon component.The memoization is appropriate here since this is a static SVG. One small thing to consider: if this is purely decorative, adding
aria-hidden="true"would let screen readers know to skip over it. Not a big deal, just a wee bit of polish.♿ Optional accessibility enhancement
- <svg viewBox="0 0 219 18" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> + <svg viewBox="0 0 219 18" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" {...props}>src/features/chains/ChainList.tsx (1)
86-104: Style conflict in ChainButton.The
styles.labelclass appliestext-sm font-normal, but Line 97 also appliestext-sm font-medium. These conflicting font weights might cause unexpected behavior depending on CSS specificity. Consider consolidating.♻️ Proposed fix - remove duplicate text-sm and clarify font weight intent
const styles = { - label: 'font-secondary text-sm font-normal', + label: 'font-secondary', };Or remove the redundant classes from the span:
- <span className="truncate text-sm font-medium">{label}</span> + <span className="truncate">{label}</span>src/features/chains/MobileChainQuickSelect.tsx (1)
44-52: Consider using a Map for chain lookups, eh?Right now ye're doing an O(n) search through
allChainsfor every preferred chain. With many chains, this could get a wee bit sluggish - like trudging through the swamp. A simple Map would make this cleaner.🔧 Minor optimization
// Get preferred chains that exist, maintaining preferred order + const chainMap = new Map(allChains.map((c) => [c.name, c])); const preferred = preferredChains .filter((name) => chainNameSet.has(name)) .map((name) => { - const chain = allChains.find((c) => c.name === name); + const chain = chainMap.get(name); return { name, displayName: chain?.displayName || name, }; });src/features/wallet/WalletDropdown.tsx (2)
50-56: Consider showing a toast on disconnect failure.Right now the error just gets logged, but the user won't know their wallet didn't actually disconnect. They might wander off thinking they're safe when they're not - like leaving me swamp gate open.
🛠️ Proposed improvement
const onDisconnect = useCallback(async () => { try { await disconnectFn?.(); } catch (err) { logger.error('Failed to disconnect wallet', err); + toast.error('Failed to disconnect wallet. Please try again.'); } }, [disconnectFn]);You'd need to import
toastfromreact-toastify.
203-215: That green dot might be a bit too subtle.
bg-green-50is quite light - might blend into the background on some displays. Consider usingbg-green-500or similar for better visibility when showing the connected state.🎨 Color suggestion
{address ? ( - <div className="h-2 w-2 rounded-full bg-green-50" /> + <div className="h-2 w-2 rounded-full bg-green-500" /> ) : (src/features/tokens/TokenList.tsx (2)
35-56:startTransitiondoesn’t make the route computation itself non-blocking.
The heavy loop still runs on the main thread; only the state update is deprioritized. IfallTokensis big, this can still hitch the UI.
125-133: Consider making “no route” tokens non-clickable (or clearly explained).
Right now they’re greyed out but still selectable; users will walk into a “Route is not supported” later.One possible tweak
- <TokenButton key={tokenKey} token={token} onSelect={onSelect} hasRoute={hasRoute} /> + <TokenButton + key={tokenKey} + token={token} + onSelect={onSelect} + hasRoute={hasRoute} + />function TokenButton({ token, onSelect, hasRoute, }:{ ... }) { ... return ( <button type="button" + disabled={!hasRoute} className={`group mb-1.5 flex w-full items-center rounded-[3px] px-3 py-2.5 transition-colors hover:bg-gray-100 ${ !hasRoute ? 'opacity-40' : '' }`} onClick={() => onSelect(token)} >Also applies to: 171-178
src/features/analytics/utils.ts (1)
70-74: Consider still tracking failures even whendestinationTokenKeycan’t be resolved.
Right now youreturnifdestTokenisn’t found, which can hide real breakages in token resolution.src/features/transfer/TransfersDetailsModal.tsx (1)
171-174: Kill the commented-out UI block once you’re sure.
Keeping dead UI in-line tends to linger like swamp fog.src/features/transfer/TransferTokenForm.tsx (1)
224-232: URL params use chain+symbol (can be ambiguous).
If two tokens share a symbol on a chain (or you add “import token”), restoring state from URL may pick the wrong one. Token keys (or address/denom) would be sturdier.Also applies to: 836-846
src/features/transfer/useFeeQuotes.ts (1)
107-117: Avoidas Tokencasts fordestinationTokenin stableswap detection.
Either widenisStableSwapRouteto acceptITokenor narrow with a real type guard—casts can hide real mismatches.tailwind.config.js (1)
124-130: Optional:error-glowuses the same color asaccent-glow.
If that’s intentional, all good; if not, you may want it keyed offredto match the name.src/features/tokens/utils.ts (3)
62-69: Theas anycast on Line 67 is a bit like covering up a hole in me swamp with a rug.The comment explains why this workaround exists (some standards like
EvmHypCollateralFiatmay not be inTOKEN_COLLATERALIZED_STANDARDS), which is good. However, thisas anycast bypasses type checking entirely. If the SDK type forstandardever changes, this won't catch it.Consider filing an issue upstream to include missing standards in
TOKEN_COLLATERALIZED_STANDARDS, or create a local extended list that's properly typed.
219-239: Minor inconsistency in key format, like having different signs for the same swamp entrance.Line 234 includes
protocolin the HypNative key (chainName-symbol-hypnative-protocol), but lines 231 and 238 don't include protocol. While collisions across protocols are rare (same chain name with same collateral address but different protocols), including protocol consistently would future-proof the key format.This is a minor nit; the current implementation should work fine for typical scenarios.
261-265: Theseas anycasts smell worse than onion layers left out in the sun.Lines 261-262 cast to
anyto accessstableSwapPool. This bypasses type safety entirely. If the SDK'sTokentype doesn't includestableSwapPool, consider:
- Extending the type locally with an interface that includes this property
- Using a type guard to safely access the property
♻️ Suggested type-safe approach
interface StableSwapToken extends Token { stableSwapPool?: string; } function hasStableSwapPool(token: Token): token is StableSwapToken { return 'stableSwapPool' in token; } // Then in isStableSwapRoute: if (!hasStableSwapPool(originToken) || !hasStableSwapPool(destToken)) { return false; } const originPool = originToken.stableSwapPool; const destPool = destToken.stableSwapPool;
| "@hyperlane-xyz/sdk": "20.2.0-stableswap.2a0d563060e1037fa228687c3c9a3ecea60dce9f", | ||
| "@hyperlane-xyz/utils": "20.2.0-stableswap.2a0d563060e1037fa228687c3c9a3ecea60dce9f", | ||
| "@hyperlane-xyz/widgets": "20.1.0", |
There was a problem hiding this comment.
Oi, these pre-release versions shouldn't wander into the main branch.
You've got @hyperlane-xyz/sdk and @hyperlane-xyz/utils pinned to 20.2.0-stableswap pre-release versions with a commit hash. That's fine for your swamp demo, but these should be reverted to published versions before this gets merged.
Also worth noting — @hyperlane-xyz/widgets is still at 20.1.0 while the others are at 20.2.0-stableswap. Might want to keep these in sync to avoid any unexpected surprises down the road.
Based on learnings, developers may temporarily use these paths for testing but should revert before merging.
🤖 Prompt for AI Agents
In @package.json around lines 24 - 26, The package.json currently pins
@hyperlane-xyz/sdk and @hyperlane-xyz/utils to pre-release stableswap builds
with commit hashes and leaves @hyperlane-xyz/widgets at 20.1.0; revert those two
entries to the published stable versions (remove the "-stableswap.*" pins and
commit hashes) and align all three @hyperlane-xyz/* packages to the same
published version (e.g., set @hyperlane-xyz/sdk, @hyperlane-xyz/utils, and
@hyperlane-xyz/widgets to the current stable release) so no pre-release or
commit-hash pins land on main.
| const defaultClassName = | ||
| 'mt-1.5 px-2.5 py-2.5 text-sm rounded-lg border border-primary-300 focus:border-primary-500 disabled:bg-gray-150 outline-none transition-all duration-300'; | ||
| 'rounded-lg border border-primary-300 focus:border-primary-500 disabled:bg-gray-150 outline-none transition-all duration-300'; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find all usages of TextField and TextInput components
echo "=== TextField usages ==="
rg -n '<TextField' --type tsx --type ts -B 2 -A 2
echo ""
echo "=== TextInput usages ==="
rg -n '<TextInput' --type tsx --type ts -B 2 -A 2Repository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 207
🏁 Script executed:
#!/bin/bash
# Find all usages of TextField and TextInput components
echo "=== TextField usages ==="
rg '<TextField' -g '*.tsx' -g '*.ts' -B 2 -A 2
echo ""
echo "=== TextInput usages ==="
rg '<TextInput' -g '*.tsx' -g '*.ts' -B 2 -A 2
echo ""
echo "=== Check for TextField/TextInput in the component file itself ==="
cat -n src/components/input/TextField.tsxRepository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 2498
🏁 Script executed:
#!/bin/bash
# Get full definitions of components using TextField/TextInput
echo "=== TransferTokenForm.tsx - TextField usage ==="
sed -n '1,150p' src/features/transfer/TransferTokenForm.tsx | grep -A 10 '<TextField'
echo ""
echo "=== SelectOrInputTokenIds.tsx - TextField usage ==="
sed -n '1,100p' src/features/tokens/SelectOrInputTokenIds.tsx | grep -A 10 '<TextField'
echo ""
echo "=== SearchInput.tsx - TextInput usage ==="
cat -n src/components/input/SearchInput.tsxRepository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 1477
🏁 Script executed:
#!/bin/bash
# Get full TransferTokenForm to see TextField usage
echo "=== TransferTokenForm.tsx - Full file to find TextField usage ==="
rg -B 5 -A 10 'TextField' src/features/transfer/TransferTokenForm.tsxRepository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 1829
Add padding and text-size classes to TextField usages—some components are left exposed.
So here's the thing: while you've stripped out the default padding and text size, not all the places using these components have gotten the memo. Take SelectOrInputTokenIds.tsx—it's only got className="w-full", which means that input's gonna feel pretty tight with no breathing room at all. Over in TransferTokenForm.tsx, there's text-xl for size, but still no padding, so it's like you've got text but nowhere comfortable for it to sit. Even SearchInput.tsx is throwing this odd all:py-2 syntax around that doesn't follow standard Tailwind conventions.
These inputs are gonna look like they're all squeezed together in a swamp with nowhere to move. Either bring back some sensible defaults or make sure every usage explicitly handles its own padding (px-* and py-*) and text sizing.
🤖 Prompt for AI Agents
In @src/components/input/TextField.tsx around lines 32 - 33, The default
TextField styling removed padding and text-size which leaves callers like
SelectOrInputTokenIds (className="w-full"), TransferTokenForm (uses text-xl but
no padding), and SearchInput (uses invalid "all:py-2") rendering cramped;
restore sensible defaults by updating the defaultClassName constant in
TextField.tsx to include padding (e.g., px-3 py-2) and a base text-size (e.g.,
text-sm or text-base), and then fix usages: either remove redundant
sizing/padding from callers or add explicit padding/text classes where needed
(update TransferTokenForm to add px/py if it wants text-xl), and replace the
invalid "all:py-2" in SearchInput with a valid Tailwind class like "py-2".
| export enum WARP_QUERY_PARAMS { | ||
| ORIGIN = 'origin', | ||
| DESTINATION = 'destination', | ||
| TOKEN = 'token', | ||
| ORIGIN_TOKEN = 'originToken', | ||
| DESTINATION_TOKEN = 'destinationToken', | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for references to the old TOKEN param and new originToken/destinationToken
echo "=== Searching for old WARP_QUERY_PARAMS.TOKEN references ==="
rg -n "WARP_QUERY_PARAMS\.TOKEN\b" src/ 2>/dev/null || echo "No matches found"
echo -e "\n=== Searching for string 'token' as query param ==="
rg -n "['\"]token['\"]" src/ | grep -v node_modules | head -20
echo -e "\n=== Searching for originToken/destinationToken usage ==="
rg -n "originToken|destinationToken" src/ | head -30
echo -e "\n=== Checking URL parsing/routing layer ==="
fd -e ts -e tsx "route|parse|query|param" src/ | head -15Repository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 3481
🏁 Script executed:
#!/bin/bash
# Find the actual args.ts file and check nearby files
echo "=== Looking at args.ts and related query param handling ==="
find src -name "*args*" -o -name "*query*" -o -name "*param*" | head -20
# Check if there's any legacy/migration code
echo -e "\n=== Searching for legacy/migration/compat handling ==="
rg -n "legacy|migration|compat|backward|fallback" src/ -i | grep -v node_modules | head -20Repository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 2423
🏁 Script executed:
#!/bin/bash
echo "=== Examining queryParams.ts ==="
cat -n src/utils/queryParams.tsRepository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 1095
Add backward compatibility handling for old ?token= query parameters.
Old bookmarks and deep links using ?token=... will silently break with this change. The queryParams utility has no migration logic to map the old single token param to the new originToken and destinationToken split. Either add a URL parsing layer that translates legacy links, or update any public documentation/tooling that generates these links.
🤖 Prompt for AI Agents
In @src/consts/args.ts around lines 3 - 8, The change removed the legacy single
"token" query parameter and breaks old deep links; update the queryParams
parsing logic to detect a legacy "token" param and map it into the new keys
(WARP_QUERY_PARAMS.ORIGIN_TOKEN and WARP_QUERY_PARAMS.DESTINATION_TOKEN) when
those new params are not present; modify the queryParams utility to prefer
explicit originToken/destinationToken but fall back to populating both from
"token" for backward compatibility, and update related tests to cover legacy and
new-param behaviors.
| interface ChainFilterPanelProps { | ||
| searchQuery: string; | ||
| onSearchChange: (s: string) => void; | ||
| selectedChain: ChainName | null; | ||
| onSelectChain: (chain: ChainName | null) => void; |
There was a problem hiding this comment.
Missing ChainName type import again.
Ye need to bring ChainName into the file, otherwise TypeScript will be grumpier than an ogre with uninvited visitors.
🐛 Proposed fix
import { ChevronIcon } from '@hyperlane-xyz/widgets';
+import { ChainName } from '@hyperlane-xyz/sdk';
import { SearchInput } from '../../components/input/SearchInput';
import { ChainList } from './ChainList';🤖 Prompt for AI Agents
In @src/features/chains/ChainFilterPanel.tsx around lines 5 - 9, The interface
ChainFilterPanelProps references the type ChainName but the file is missing its
import; add an import for the ChainName type (the same exported symbol used
elsewhere in the project) at the top of the file so TypeScript recognizes
ChainName and the props signature for ChainFilterPanelProps compiles correctly.
| <Modal isOpen={isOpen} close={close} panelClassname="p-0 max-w-sm md:max-w-128 overflow-hidden"> | ||
| <ModalHeader>Fee Details</ModalHeader> | ||
| <div className="flex w-full flex-col items-start gap-2 p-4 text-sm"> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find and check the TransferFeeModal.tsx file
fd -t f "TransferFeeModal.tsx" -x cat -n {}Repository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 3857
🏁 Script executed:
# Search for Tailwind config files
fd -t f "tailwind\\.config\\.(js|ts|cjs|mjs)" -o "tailwind.config.ts" -o "tailwind.config.js"Repository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 239
🏁 Script executed:
# Search for max-w-128 definition across the codebase
rg "max-w-128" -A 2 -B 2Repository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 535
🏁 Script executed:
# Search for Tailwind config at root
fd "tailwind.config" -t fRepository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 102
🏁 Script executed:
# Also check for any custom CSS files that might define max-w-128
rg "max-w-128|128" --type css -B 2 -A 2Repository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 66
🏁 Script executed:
# Check tsconfig and package.json to understand project structure better
cat -n tailwind.config.ts tailwind.config.js 2>/dev/null | head -50Repository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 1695
🏁 Script executed:
# Read the complete tailwind config file
cat tailwind.config.jsRepository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 4238
Add max-w-128 to your Tailwind config's maxWidth extend.
The max-w-128 class you're using on line 19 isn't defined in your tailwind.config.js—only the spacing utility has 128. The class won't be generated, so your responsive max-width won't work as intended. Pop this into the maxWidth section under extend:
maxWidth: {
'xl-1': '39.5rem',
128: '32rem', // Add this
},This way the modal will respect the width constraint on medium screens and up.
🤖 Prompt for AI Agents
In @src/features/transfer/TransferFeeModal.tsx around lines 19 - 21, The
Tailwind class "max-w-128" used on the Modal component (Modal isOpen={isOpen}
... panelClassname="p-0 max-w-sm md:max-w-128 overflow-hidden") isn't defined in
your tailwind config so it won't be generated; open your tailwind.config.js and
add a 128 entry under theme.extend.maxWidth (e.g., map 128 to '32rem' or your
desired value) so the "md:max-w-128" utility exists and the modal width will
behave as expected.
| // Update origin chain name in store when origin token changes | ||
| useEffect(() => { | ||
| if (!originChainName) setOriginChainName(initialValues.origin); | ||
| }, [initialValues.origin, originChainName, setOriginChainName]); | ||
| const originToken = getTokenByKey(tokens, initialValues.originTokenKey); | ||
| if (originToken) { | ||
| setOriginChainName(originToken.chainName); | ||
| } | ||
| }, [initialValues.originTokenKey, tokens, setOriginChainName]); | ||
|
|
There was a problem hiding this comment.
Bug: origin chain store updates only for initial token, not user changes.
This effect listens to initialValues.originTokenKey, which won’t change when the user picks a new origin token in Formik.
Fix idea: drive it off Formik `values.originTokenKey`
- // Update origin chain name in store when origin token changes
- useEffect(() => {
- const originToken = getTokenByKey(tokens, initialValues.originTokenKey);
- if (originToken) {
- setOriginChainName(originToken.chainName);
- }
- }, [initialValues.originTokenKey, tokens, setOriginChainName]);
+ // Update origin chain name in store when origin token changes
+ // (must follow Formik state, not the initial snapshot)
+ // Move into a child component that can use `useFormikContext`,
+ // or add a small component here under <Formik>.🤖 Prompt for AI Agents
In @src/features/transfer/TransferTokenForm.tsx around lines 148 - 155, The
effect currently uses initialValues.originTokenKey so it only runs for the
initial selection; change it to read the live Formik value instead (e.g. use
useFormikContext() or receive values/originTokenKey prop) so it reacts to user
changes: inside the useEffect that calls getTokenByKey(tokens, ...), replace
initialValues.originTokenKey with the current Formik values.originTokenKey and
keep setOriginChainName(originToken.chainName) as before so the origin chain
store updates whenever the user picks a different origin token.
| function MaxButton({ | ||
| balance, | ||
| disabled, | ||
| isRouteSupported, | ||
| }: { | ||
| balance?: TokenAmount; | ||
| disabled?: boolean; | ||
| isRouteSupported: boolean; | ||
| }) { | ||
| const { values, setFieldValue } = useFormikContext<TransferFormValues>(); | ||
| const { originTokenKey, destinationTokenKey } = values; | ||
| const tokens = useTokens(); | ||
| const originToken = getTokenByKey(tokens, originTokenKey); | ||
| const destinationToken = getTokenByKey(tokens, destinationTokenKey); | ||
| const multiProvider = useMultiProvider(); | ||
| const { accounts } = useAccounts(multiProvider); | ||
| const { fetchMaxAmount, isLoading } = useFetchMaxAmount(); | ||
|
|
||
| const isDisabled = | ||
| disabled || !isRouteSupported || isLoading || !balance || !originToken || !destinationToken; | ||
|
|
||
| const onClick = async () => { | ||
| if (isDisabled) return; | ||
| const maxAmount = await fetchMaxAmount({ | ||
| balance, | ||
| origin: originToken.chainName, | ||
| destination: destinationToken.chainName, | ||
| accounts, | ||
| recipient: values.recipient, | ||
| }); | ||
| if (isNullish(maxAmount)) return; | ||
| const decimalsAmount = maxAmount.getDecimalFormattedAmount(); | ||
| const roundedAmount = new BigNumber(decimalsAmount).toFixed(4, BigNumber.ROUND_FLOOR); | ||
| setFieldValue('amount', roundedAmount); | ||
| }; |
There was a problem hiding this comment.
Max button should use the same “effective recipient” fallback as fees/validation.
Right now it passes values.recipient straight through; if the user relies on the connected destination wallet default, Max may never work.
Minimal fix (compute effective recipient like elsewhere)
const onClick = async () => {
if (isDisabled) return;
+ const { address: connectedDestAddress } = getAccountAddressAndPubKey(
+ multiProvider,
+ destinationToken.chainName,
+ accounts,
+ );
+ const recipient = values.recipient || connectedDestAddress || '';
+ if (!recipient) return;
const maxAmount = await fetchMaxAmount({
balance,
origin: originToken.chainName,
destination: destinationToken.chainName,
accounts,
- recipient: values.recipient,
+ recipient,
});🤖 Prompt for AI Agents
In @src/features/transfer/TransferTokenForm.tsx around lines 378 - 412, The
MaxButton currently passes values.recipient directly to fetchMaxAmount which
breaks when the UI relies on the connected destination wallet default; compute
the same "effective recipient" used by fees/validation and pass that to
fetchMaxAmount instead of values.recipient—i.e., derive the fallback recipient
from the connected accounts/selected destination token (same logic used
elsewhere in the form) and use that value in the fetchMaxAmount call inside
MaxButton before formatting and setFieldValue('amount', ...).
| import { isValidAddress, ProtocolType } from '@hyperlane-xyz/utils'; | ||
| import { Modal, XIcon } from '@hyperlane-xyz/widgets'; | ||
| import { useState } from 'react'; | ||
| import { SolidButton } from '../../components/buttons/SolidButton'; | ||
|
|
There was a problem hiding this comment.
This will fail to compile: React.ChangeEvent used without importing React types.
Bring in ChangeEvent (or type React) from react so the type reference exists.
Proposed fix
import { isValidAddress, ProtocolType } from '@hyperlane-xyz/utils';
import { Modal, XIcon } from '@hyperlane-xyz/widgets';
-import { useState } from 'react';
+import { useState, type ChangeEvent } from 'react';
import { SolidButton } from '../../components/buttons/SolidButton';
@@
- const handleAddressChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ const handleAddressChange = (e: ChangeEvent<HTMLInputElement>) => {
setAddress(e.target.value);
if (error) setError('');
};Also applies to: 44-47
🤖 Prompt for AI Agents
In @src/features/wallet/RecipientAddressModal.tsx around lines 1 - 5, The file
uses React.ChangeEvent types but doesn't import React types; update the imports
at the top of RecipientAddressModal (where
isValidAddress/ProtocolType/Modal/XIcon/useState/SolidButton are imported) to
include the type import (e.g., import type { ChangeEvent } from 'react' or
import React and use React.ChangeEvent) so the type references in the component
(e.g., the onChange/handleChange handlers around the RecipientAddressModal form,
including the code referenced in lines ~44-47) resolve and the module compiles.
| /* | ||
| Custom Fonts (optional - falls back to system fonts if not available) | ||
| Place font files in public/fonts/ directory | ||
| ===================================================================== | ||
| */ | ||
|
|
||
| /* PP Valve - Secondary font */ | ||
| @font-face { | ||
| font-family: 'PP Valve'; | ||
| src: url('/fonts/PPValve-PlainVariable.woff2') format('woff2'); | ||
| font-weight: 100 900; | ||
| font-style: normal; | ||
| font-display: swap; | ||
| } | ||
|
|
||
| /* PP Fraktion Mono - Primary font */ | ||
| @font-face { | ||
| font-family: 'PP Fraktion Mono'; | ||
| src: url('/fonts/PPFraktionMono-Variable.woff2') format('woff2'); | ||
| font-weight: 100 900; | ||
| font-style: normal; | ||
| font-display: swap; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check public/fonts directory structure and font files
find public/fonts -type f 2>/dev/null | head -20 || echo "No public/fonts directory found"Repository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 66
🏁 Script executed:
#!/bin/bash
# Also check if public directory exists and its structure
ls -la public/ 2>/dev/null | head -20 || echo "No public directory found"Repository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 1079
🏁 Script executed:
#!/bin/bash
# Search for .woff2 font files anywhere in repo
find . -name "*.woff2" 2>/dev/nullRepository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 66
🏁 Script executed:
#!/bin/bash
# Also check if there's a fonts directory anywhere
find . -type d -name "fonts" 2>/dev/nullRepository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 66
Those font files you're referencing? Yeah, they're not actually in the repository.
Look, I'm not tryin' to be difficult here, but the CSS is pointin' to /fonts/PPValve-PlainVariable.woff2 and /fonts/PPFraktionMono-Variable.woff2, and they don't exist nowhere in the codebase. There's no public/fonts/ directory at all.
The @font-face declarations are properly written with the variable font syntax and font-display: swap is a nice touch for performance, but without the actual .woff2 files, they're not gonna load. Sure, the CSS comment mentions fallin' back to system fonts, which technically happens, but that means users won't get your custom fonts.
You need to either:
- Add those font files to
public/fonts/directory, or - Point the CSS to wherever you're actually keepin' 'em
Get those fonts in place and this'll be set.
🤖 Prompt for AI Agents
In @src/styles/globals.css around lines 5 - 27, The @font-face rules in
globals.css reference '/fonts/PPValve-PlainVariable.woff2' and
'/fonts/PPFraktionMono-Variable.woff2' but those files and the public/fonts
directory are missing; fix by either adding the actual .woff2 files to the
public/fonts directory and committing them, or update the src URLs in the 'PP
Valve' and 'PP Fraktion Mono' @font-face declarations to the correct existing
asset paths (or a CDN) and verify the files are served in production; ensure
font filenames in the CSS exactly match the committed asset names and that you
keep font-display: swap.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/consts/warpRoutes.yaml`:
- Around line 122-153: The Linea and Mantra entries (chainName: linea and
chainName: mantra) currently reuse the same token address in the fields
addressOrDenom and adapterAddress (0xb9Fc2748D7f4d81cfc2ca78a52f52b4ADf1C4895);
update these fields so each chain uses its correct deployed token and adapter
addresses (or remove adapterAddress if not applicable) instead of the copied
value — change addressOrDenom and adapterAddress in the linea block and/or
mantra block to the proper per-chain addresses to match their
helperAddress/collateralAddressOrDenom differences.
| - chainName: linea | ||
| standard: EvmHypStableSwap | ||
| decimals: 6 | ||
| symbol: mUSD | ||
| name: MetaMask USD | ||
| addressOrDenom: '0xb9Fc2748D7f4d81cfc2ca78a52f52b4ADf1C4895' | ||
| collateralAddressOrDenom: '0xaca92e438df0b2401ff60da7e4337b687a2435da' | ||
| stableSwapPool: mainnet-stableswap-pool | ||
| helperAddress: '0x3856c8A14cf1D1da69237B8e79aDb4E3eE44fba2' | ||
| sUSDAddress: '0x38E8720EBE02e7c5254F9De9F81440C7a770a9c6' | ||
| adapterAddress: '0xb9Fc2748D7f4d81cfc2ca78a52f52b4ADf1C4895' | ||
| connections: | ||
| - token: ethereum|arbitrum|0x94C62e7958738B65737a0Db8A5077def3AED84AA | ||
| - token: ethereum|arbitrum|0x6BEC839292A36372882Cb850E93FB5aC2A9BA4Af | ||
| - token: ethereum|base|0x73Ef899fDa87213e26501707ab585028BFB297c8 | ||
| - token: ethereum|base|0x23c51024b19303F1315DbFFA055666aE9B7A0B2c | ||
| - token: ethereum|soneium|0x867D428B8FbE196EA4e997e7980623E75ED219a7 | ||
| - token: ethereum|hyperevm|0xc7d07C8acFC66852BAad9Afb49E8A3AA7F6D3575 | ||
| - token: ethereum|mantra|0xb9Fc2748D7f4d81cfc2ca78a52f52b4ADf1C4895 | ||
|
|
||
| # MANTRA mantraUSD | ||
| - chainName: mantra | ||
| standard: EvmHypStableSwap | ||
| decimals: 6 | ||
| symbol: mantraUSD | ||
| name: MANTRA USD | ||
| addressOrDenom: '0xb9Fc2748D7f4d81cfc2ca78a52f52b4ADf1C4895' | ||
| collateralAddressOrDenom: '0xd2b95283011E47257917770D28Bb3EE44c849f6F' | ||
| stableSwapPool: mainnet-stableswap-pool | ||
| helperAddress: '0xF0d9D232141dF68DD3CE272dd216C8D71645F25A' | ||
| sUSDAddress: '0x38E8720EBE02e7c5254F9De9F81440C7a770a9c6' | ||
| adapterAddress: '0xb9Fc2748D7f4d81cfc2ca78a52f52b4ADf1C4895' |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for the shared address across all file types
rg -n "0xb9Fc2748D7f4d81cfc2ca78a52f52b4ADf1C4895" --type-list | head -5
rg -n "0xb9Fc2748D7f4d81cfc2ca78a52f52b4ADf1C4895"Repository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 1499
🏁 Script executed:
#!/bin/bash
# Also search for the Linea-specific chain config and Mantra-specific chain config
# to understand if this is a legitimate cross-chain address
rg -B5 -A10 "chainName: linea" src/consts/warpRoutes.yaml
rg -B5 -A10 "chainName: mantra" src/consts/warpRoutes.yamlRepository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 1538
🏁 Script executed:
#!/bin/bash
# Check git history to see if there's a copy/paste pattern or comments
git log --oneline -n 20 -- src/consts/warpRoutes.yaml 2>/dev/null || echo "Git history not available"Repository: hyperlane-xyz/hyperlane-warp-ui-template
Length of output: 106
Fix the duplicated token address across Linea and Mantra. Both addressOrDenom and adapterAddress use 0xb9Fc2748D7f4d81cfc2ca78a52f52b4ADf1C4895 — the same address for different chains. That's a copy/paste slip waiting to happen. Each chain needs its own token deployment, not the same address. The fact that helperAddress and collateralAddressOrDenom are different between the two entries shows they were meant to be customized, so the primary address should be too.
🤖 Prompt for AI Agents
In `@src/consts/warpRoutes.yaml` around lines 122 - 153, The Linea and Mantra
entries (chainName: linea and chainName: mantra) currently reuse the same token
address in the fields addressOrDenom and adapterAddress
(0xb9Fc2748D7f4d81cfc2ca78a52f52b4ADf1C4895); update these fields so each chain
uses its correct deployed token and adapter addresses (or remove adapterAddress
if not applicable) instead of the copied value — change addressOrDenom and
adapterAddress in the linea block and/or mantra block to the proper per-chain
addresses to match their helperAddress/collateralAddressOrDenom differences.
- Update all helper addresses to new deployments (Feb 2026) - Add missing connections for arbitrum/base USDC/USDT to hyperevm, linea, mantra - All 8 tokens now have full mesh connectivity New helper addresses: - arbitrum: 0x56db6AF01e03E31695F847633F0F15eF857D4179 - base: 0x9650deA331801F944335669a8569ed39f7AEf342 - soneium: 0xcA45246445A047670B67F87c5AC75A4DbE5134F1 - mantra: 0xF0d9D232141dF68DD3CE272dd216C8D71645F25A - linea: 0x3856c8A14cf1D1da69237B8e79aDb4E3eE44fba2 - hyperevm: 0x7e97865321a4a16B7DDc4ea5387E63ED1999b6d7 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/consts/warpRoutes.yaml`:
- Around line 112-132: The hyperevm token entry (chainName: hyperevm, symbol:
USDH, name: USD Hyperliquid) is missing a logoURI; add a logoURI field to this
YAML object (alongside addressOrDenom, helperAddress, etc.) with the appropriate
image URL used by other tokens (e.g., the project CDN or repo path) so the UI
can display the token icon; ensure the new key is a string named logoURI and
follows the same formatting/location as other entries in this file.
| # HyperEVM USDH | ||
| - chainName: hyperevm | ||
| standard: EvmHypStableSwap | ||
| decimals: 6 | ||
| symbol: USDH | ||
| name: USD Hyperliquid | ||
| addressOrDenom: '0xc7d07C8acFC66852BAad9Afb49E8A3AA7F6D3575' | ||
| collateralAddressOrDenom: '0x111111a1a0667d36bd57c0a9f569b98057111111' | ||
| stableSwapPool: mainnet-stableswap-pool | ||
| helperAddress: '0x7e97865321a4a16B7DDc4ea5387E63ED1999b6d7' | ||
| sUSDAddress: '0x0a089A151228Fd8CdfB1082a12b030D4C064F497' | ||
| adapterAddress: '0xc7d07C8acFC66852BAad9Afb49E8A3AA7F6D3575' | ||
| connections: | ||
| - token: ethereum|arbitrum|0x94C62e7958738B65737a0Db8A5077def3AED84AA | ||
| - token: ethereum|arbitrum|0x6BEC839292A36372882Cb850E93FB5aC2A9BA4Af | ||
| - token: ethereum|base|0x73Ef899fDa87213e26501707ab585028BFB297c8 | ||
| - token: ethereum|base|0x23c51024b19303F1315DbFFA055666aE9B7A0B2c | ||
| - token: ethereum|soneium|0x867D428B8FbE196EA4e997e7980623E75ED219a7 | ||
| - token: ethereum|linea|0xb9Fc2748D7f4d81cfc2ca78a52f52b4ADf1C4895 | ||
| - token: ethereum|mantra|0xb9Fc2748D7f4d81cfc2ca78a52f52b4ADf1C4895 | ||
|
|
There was a problem hiding this comment.
Missing logoURI for HyperEVM USDH entry.
Unlike the other token entries in this file, the HyperEVM USDH configuration doesn't have a logoURI field. This might leave users staring at a blank spot where an icon should be—nobody wants that kind of emptiness in their swamp.
🔧 Proposed fix to add the missing logoURI
# HyperEVM USDH
- chainName: hyperevm
standard: EvmHypStableSwap
decimals: 6
symbol: USDH
name: USD Hyperliquid
addressOrDenom: '0xc7d07C8acFC66852BAad9Afb49E8A3AA7F6D3575'
collateralAddressOrDenom: '0x111111a1a0667d36bd57c0a9f569b98057111111'
+ logoURI: <ADD_APPROPRIATE_LOGO_URI_HERE>
stableSwapPool: mainnet-stableswap-pool🤖 Prompt for AI Agents
In `@src/consts/warpRoutes.yaml` around lines 112 - 132, The hyperevm token entry
(chainName: hyperevm, symbol: USDH, name: USD Hyperliquid) is missing a logoURI;
add a logoURI field to this YAML object (alongside addressOrDenom,
helperAddress, etc.) with the appropriate image URL used by other tokens (e.g.,
the project CDN or repo path) so the UI can display the token icon; ensure the
new key is a string named logoURI and follows the same formatting/location as
other entries in this file.
- Default registry to stableswap branch for preview deployments - Whitelist sUSD/stableswap-pool route from registry - Clear local warpRoutes.yaml (routes come from registry) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove env var fallback to ensure this preview branch always uses the stableswap-extension-deploy registry branch. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary by CodeRabbit
New Features
Refactor
Style
Bug Fixes
Chores