Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CUSTOMIZE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Find below instructions for customizing the token list and branding assets of th

## Registry

By default, the app will use the canonical Hyperlane registry published on NPM. See `package.json` for the precise version.
By default, this branch uses the Hyperlane GitHub registry at `https://github.com/hyperlane-xyz/hyperlane-registry` on branch `nambrot/multi-collateral-deploy`.

To use custom chains or custom warp routes, you can either configure a different registry using the `NEXT_PUBLIC_REGISTRY_URL` environment variable or define them manually (see the next two sections).
Comment on lines +7 to 9

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Doc drift in this registry section.

src/consts/config.ts now pins codex/nambrot-cross-collateral-deploy and disables NEXT_PUBLIC_REGISTRY_* overrides, but this paragraph still points folks at nambrot/multi-collateral-deploy and NEXT_PUBLIC_REGISTRY_URL. That’ll send customizers up the wrong hill.

📝 Suggested doc fix
-By default, this branch uses the Hyperlane GitHub registry at `https://github.com/hyperlane-xyz/hyperlane-registry` on branch `nambrot/multi-collateral-deploy`.
+By default, this branch uses the Hyperlane GitHub registry at `https://github.com/hyperlane-xyz/hyperlane-registry` on branch `codex/nambrot-cross-collateral-deploy`.

-To use custom chains or custom warp routes, you can either configure a different registry using the `NEXT_PUBLIC_REGISTRY_URL` environment variable or define them manually (see the next two sections).
+To use a different registry in this preview branch, update `src/consts/config.ts`, or define custom chains and custom warp routes manually (see the next two sections).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CUSTOMIZE.md` around lines 7 - 9, Update the registry paragraph in
CUSTOMIZE.md to match the current implementation in src/consts/config.ts:
replace the reference to the Hyperlane registry branch
`nambrot/multi-collateral-deploy` with the pinned
`codex/nambrot-cross-collateral-deploy`, and explicitly state that
NEXT_PUBLIC_REGISTRY_URL and other NEXT_PUBLIC_REGISTRY_* overrides are disabled
by the code so they cannot be used; instead explain the supported alternatives
(manually define custom chains/warp routes or follow the repo’s pinned registry)
and point readers to src/consts/config.ts for the authoritative source.


Expand Down
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@headlessui/react": "^2.2.0",
"@hyperlane-xyz/deploy-sdk": "4.0.0",
"@hyperlane-xyz/provider-sdk": "4.0.0",
"@hyperlane-xyz/registry": "24.0.0",
"@hyperlane-xyz/sdk": "25.5.0",
"@hyperlane-xyz/utils": "25.5.0",
"@hyperlane-xyz/widgets": "25.5.0",
"@hyperlane-xyz/sdk": "28.0.0",
"@hyperlane-xyz/utils": "28.0.0",
"@hyperlane-xyz/widgets": "28.0.0",
"@interchain-ui/react": "^1.23.28",
"@intercom/messenger-js-sdk": "^0.0.18",
"@metamask/post-message-stream": "6.1.2",
Expand Down Expand Up @@ -152,7 +154,8 @@
"cipher-base": "1.0.5",
"elliptic": "6.6.1",
"pbkdf2": "3.1.3",
"form-data": "4.0.4"
"form-data": "4.0.4",
"@hyperlane-xyz/core": "11.0.3"
},
"patchedDependencies": {
"starknetkit@2.6.1": "patches/starknetkit@2.6.1.patch"
Expand Down
6,347 changes: 4,828 additions & 1,519 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions src/consts/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import { ADDRESS_BLACKLIST } from './blacklist';

const isDevMode = process?.env?.NODE_ENV === 'development';
const version = process?.env?.NEXT_PUBLIC_VERSION || '2.0.0';
const registryUrl = process?.env?.NEXT_PUBLIC_REGISTRY_URL || undefined;
const registryBranch = process?.env?.NEXT_PUBLIC_REGISTRY_BRANCH || undefined;
const registryProxyUrl = process?.env?.NEXT_PUBLIC_GITHUB_PROXY || 'https://proxy.hyperlane.xyz';
const DEFAULT_REGISTRY_URL = 'https://github.com/hyperlane-xyz/hyperlane-registry';
const DEFAULT_REGISTRY_BRANCH = 'codex/nambrot-cross-collateral-deploy';
// Keep registry source pinned in code for this MC preview flow.
// We intentionally do not allow NEXT_PUBLIC_REGISTRY_* env overrides.
const registryUrl = DEFAULT_REGISTRY_URL;
const registryBranch = DEFAULT_REGISTRY_BRANCH;
const registryProxyUrl = 'https://proxy.hyperlane.xyz';
const walletConnectProjectId = process?.env?.NEXT_PUBLIC_WALLET_CONNECT_ID || '';
const transferBlacklist = process?.env?.NEXT_PUBLIC_TRANSFER_BLACKLIST || '';
const chainWalletWhitelists = JSON.parse(process?.env?.NEXT_PUBLIC_CHAIN_WALLET_WHITELISTS || '{}');
Expand Down
7 changes: 6 additions & 1 deletion src/consts/warpRouteWhitelist.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ import {
import { WarpCoreConfig } from '@hyperlane-xyz/sdk';
import { objKeys } from '@hyperlane-xyz/utils';
import { assert, test } from 'vitest';
import { config } from './config';
import { warpRouteWhitelist } from './warpRouteWhitelist';

test('warpRouteWhitelist', async () => {
if (!warpRouteWhitelist) return;

const registry = new GithubRegistry();
const registry = new GithubRegistry({
uri: config.registryUrl,
branch: config.registryBranch,
proxyUrl: config.registryProxyUrl,
});
Comment on lines +14 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

This test still misses the new per-route recovery path.

assembleWarpCoreConfig() now repairs missing whitelist IDs with registry.getWarpRoute(routeId), but this test only checks getWarpRoutes() or the published snapshot. If a whitelisted route exists only as a per-route file, CI goes red even though the app would load it. I’d either assert through assembleWarpCoreConfig() or mirror the same missing-ID recovery here.

Also applies to: 21-29

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/consts/warpRouteWhitelist.test.ts` around lines 14 - 18, The test
currently only validates registry.getWarpRoutes()/snapshot and misses the new
per-route recovery path; update the test to either call assembleWarpCoreConfig()
(so the missing-ID repair runs) or stub GithubRegistry.getWarpRoute(routeId) to
return the per-route definition for a missing whitelist ID and then assert the
resulting whitelist includes that recovered route; target the GithubRegistry
instance constructed in the test and the assembleWarpCoreConfig function to
ensure the per-route recovery behavior is exercised.

let warpRouteConfigs: Record<string, WarpCoreConfig>;

try {
Expand Down
6 changes: 1 addition & 5 deletions src/consts/warpRouteWhitelist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,4 @@
// Warp Route IDs use format `SYMBOL/chainname1-chainname2...` where chains are ordered alphabetically
// If left null, all warp routes in the configured registry will be included
// If set to a list (including an empty list), only the specified routes will be included
export const warpRouteWhitelist: Array<string> | null = null;
// Example:
// [
// // 'ETH/ethereum-viction'
// ];
export const warpRouteWhitelist: Array<string> | null = ['CROSS/ctusd'];
5 changes: 1 addition & 4 deletions src/consts/warpRoutes.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# A list of Warp Route token configs
# These configs will be merged with the warp routes in the configured registry
# The input here is typically the output of the Hyperlane CLI warp deploy command
---
# yaml-language-server: $schema=../schema.json
tokens: []
options: {}
13 changes: 12 additions & 1 deletion src/features/tokens/TokenSelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ type Props = {
disabled?: boolean;
setIsNft?: (value: boolean) => void;
showLabel?: boolean;
// TEMP(mc-preview-collateral-tooltip): Remove once temporary collateral hover UI is no longer needed.
hoverTooltipContent?: string;
};

export function TokenSelectField({
Expand All @@ -32,6 +34,7 @@ export function TokenSelectField({
disabled,
setIsNft,
showLabel = true,
hoverTooltipContent,
}: Props) {
const { values, setFieldValue } = useFormikContext<TransferFormValues>();
const [{ value: tokenKey }, , { setValue: setTokenKey }] = useField<string | undefined>(name);
Expand Down Expand Up @@ -141,6 +144,7 @@ export function TokenSelectField({
disabled={disabled}
onClick={openTokenSelectModal}
multiProvider={multiProvider}
hoverTooltipContent={hoverTooltipContent}
/>
</div>

Expand Down Expand Up @@ -170,11 +174,13 @@ function TokenButton({
disabled,
onClick,
multiProvider,
hoverTooltipContent,
}: {
token?: Token;
disabled?: boolean;
onClick: () => void;
multiProvider: ReturnType<typeof useMultiProvider>;
hoverTooltipContent?: string;
}) {
const chainDisplayName = token ? getChainDisplayName(multiProvider, token.chainName) : '';

Expand All @@ -185,6 +191,11 @@ function TokenButton({
onClick={onClick}
disabled={disabled}
>
{hoverTooltipContent && (
<span className="pointer-events-none absolute -top-2 left-1/2 z-20 w-max max-w-[260px] -translate-x-1/2 -translate-y-full rounded-md bg-gray-900 px-2 py-1 text-left font-secondary text-xs leading-tight text-white opacity-0 shadow-lg transition-opacity duration-150 group-hover:opacity-100">
{hoverTooltipContent}
</span>
)}
{token ? (
<div className="flex min-w-0 flex-1 items-center gap-2.5">
<TokenChainIcon token={token} size={36} />
Expand All @@ -204,7 +215,7 @@ function TokenButton({
}

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',
base: 'relative 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',
disabled: 'cursor-not-allowed opacity-60',
};
116 changes: 116 additions & 0 deletions src/features/tokens/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
buildTokensArray,
checkTokenHasRoute,
dedupeTokensByCollateral,
findConnectedDestinationToken,
findRouteToken,
getTokenKey,
groupTokensByCollateral,
Expand Down Expand Up @@ -722,6 +723,36 @@ describe('checkTokenHasRoute', () => {
expect(checkTokenHasRoute(origin, dest, groups)).toBe(true);
});

test('should return true when a later same-chain connection matches dest collateral', () => {
const origin = createMockToken({
chainName: 'ethereum',
addressOrDenom: ADDR_1,
collateralAddressOrDenom: COLLATERAL_A,
connections: [
createTokenConnectionMock(undefined, {
chainName: 'arbitrum',
addressOrDenom: '0x3333333333333333333333333333333333333333',
// First same-chain connection points to a different collateral
collateralAddressOrDenom: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
}),
createTokenConnectionMock(undefined, {
chainName: 'arbitrum',
addressOrDenom: ADDR_2,
// Second same-chain connection is the intended collateral
collateralAddressOrDenom: COLLATERAL_B,
}),
],
});
const dest = createMockToken({
chainName: 'arbitrum',
addressOrDenom: ADDR_2,
collateralAddressOrDenom: COLLATERAL_B,
});

const groups = groupTokensByCollateral([origin, dest]);
expect(checkTokenHasRoute(origin, dest, groups)).toBe(true);
});

test('should return false when no connection to dest chain', () => {
const origin = createMockToken({
chainName: 'ethereum',
Expand Down Expand Up @@ -839,6 +870,45 @@ describe('findRouteToken', () => {
expect(warpCore.getTokensForRoute).not.toHaveBeenCalled();
});

test('should prefer route token that matches specific destination token on same chain', () => {
const destinationCollateralA = '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
const destinationCollateralB = '0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB';
const origin = createMockToken({
chainName: 'ethereum',
addressOrDenom: ADDR_1,
collateralAddressOrDenom: COLLATERAL,
connections: [
createTokenConnectionMock(undefined, {
chainName: 'arbitrum',
addressOrDenom: ADDR_2,
collateralAddressOrDenom: destinationCollateralA,
}),
],
});
const selectedDestination = createMockToken({
chainName: 'arbitrum',
addressOrDenom: ADDR_3,
collateralAddressOrDenom: destinationCollateralB,
});
const routeToken = createMockToken({
chainName: 'ethereum',
addressOrDenom: '0x4444444444444444444444444444444444444444',
collateralAddressOrDenom: COLLATERAL,
connections: [
createTokenConnectionMock(undefined, {
chainName: 'arbitrum',
addressOrDenom: ADDR_3,
collateralAddressOrDenom: destinationCollateralB,
}),
],
});
const warpCore = createMockWarpCore([routeToken]);

const result = findRouteToken(warpCore, origin, 'arbitrum', selectedDestination);

expect(result).toBe(routeToken);
});

test('should return undefined when no routes exist', () => {
const origin = createMockToken({
chainName: 'ethereum',
Expand Down Expand Up @@ -1218,3 +1288,49 @@ describe('resolved underlying map integration', () => {
expect(checkTokenHasRoute(regularUsdt, destToken, groups)).toBe(false);
});
});

describe('findConnectedDestinationToken', () => {
test('should match later same-chain connection by collateral key', () => {
const COLLATERAL_A = '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
const COLLATERAL_B = '0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB';
const origin = createMockToken({
chainName: 'ethereum',
collateralAddressOrDenom: COLLATERAL_A,
connections: [
createTokenConnectionMock(undefined, {
chainName: 'arbitrum',
addressOrDenom: '0x1111111111111111111111111111111111111111',
collateralAddressOrDenom: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
}),
createTokenConnectionMock(undefined, {
chainName: 'arbitrum',
addressOrDenom: '0x2222222222222222222222222222222222222222',
collateralAddressOrDenom: COLLATERAL_B,
}),
],
});
const selectedDestination = createMockToken({
chainName: 'arbitrum',
addressOrDenom: '0x3333333333333333333333333333333333333333',
collateralAddressOrDenom: COLLATERAL_B,
});

const matched = findConnectedDestinationToken(origin, selectedDestination);
expect(matched?.addressOrDenom).toBe('0x2222222222222222222222222222222222222222');
});

test('should return undefined when there is no destination-chain connection', () => {
const origin = createMockToken({
chainName: 'ethereum',
connections: [
createTokenConnectionMock(undefined, {
chainName: 'optimism',
}),
],
});
const selectedDestination = createMockToken({ chainName: 'arbitrum' });

const matched = findConnectedDestinationToken(origin, selectedDestination);
expect(matched).toBeUndefined();
});
});
Loading