- {t.token.addressOrDenom || 'Native chain token'}
+ {t.token.collateralAddressOrDenom || t.token.addressOrDenom || 'Native chain token'}
{`Decimals: ${t.token.decimals}`}
diff --git a/src/features/tokens/warpCoreConfig.ts b/src/features/tokens/warpCoreConfig.ts
deleted file mode 100644
index 81a61e8c..00000000
--- a/src/features/tokens/warpCoreConfig.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { warpRouteConfigs } from '@hyperlane-xyz/registry';
-import { WarpCoreConfig, WarpCoreConfigSchema, validateZodResult } from '@hyperlane-xyz/sdk';
-import { objFilter, objMerge } from '@hyperlane-xyz/utils';
-import { warpRouteWhitelist } from '../../consts/warpRouteWhitelist.ts';
-import { warpRouteConfigs as WarpRoutesTs } from '../../consts/warpRoutes.ts';
-import WarpRoutesYaml from '../../consts/warpRoutes.yaml';
-
-export function assembleWarpCoreConfig(): WarpCoreConfig {
- const resultYaml = WarpCoreConfigSchema.safeParse(WarpRoutesYaml);
- const configYaml = validateZodResult(resultYaml, 'warp core yaml config');
- const resultTs = WarpCoreConfigSchema.safeParse(WarpRoutesTs);
- const configTs = validateZodResult(resultTs, 'warp core typescript config');
-
- const filteredWarpRouteConfigs = warpRouteWhitelist
- ? filterToIds(warpRouteConfigs, warpRouteWhitelist)
- : warpRouteConfigs;
-
- const configValues = Object.values(filteredWarpRouteConfigs);
-
- const configTokens = configValues.map((c) => c.tokens).flat();
- const tokens = dedupeTokens([...configTokens, ...configTs.tokens, ...configYaml.tokens]);
-
- if (!tokens.length)
- throw new Error(
- 'No warp route configs provided. Please check your registry, warp route whitelist, and custom route configs for issues.',
- );
-
- const configOptions = configValues.map((c) => c.options).flat();
- const combinedOptions = [...configOptions, configTs.options, configYaml.options];
- const options = combinedOptions.reduce
((acc, o) => {
- if (!o || !acc) return acc;
- for (const key of Object.keys(o)) {
- acc[key] = (acc[key] || []).concat(o[key] || []);
- }
- return acc;
- }, {});
-
- return { tokens, options };
-}
-
-function filterToIds(
- config: Record,
- idWhitelist: string[],
-): Record {
- return objFilter(config, (id, c): c is WarpCoreConfig => idWhitelist.includes(id));
-}
-
-// Separate warp configs may contain duplicate definitions of the same token.
-// E.g. an IBC token that gets used for interchain gas in many different routes.
-function dedupeTokens(tokens: WarpCoreConfig['tokens']): WarpCoreConfig['tokens'] {
- const idToToken: Record = {};
- for (const token of tokens) {
- const id = `${token.chainName}|${token.addressOrDenom?.toLowerCase()}`;
- idToToken[id] = objMerge(idToToken[id] || {}, token);
- }
- return Object.values(idToToken);
-}
diff --git a/src/features/wallet/WalletFloatingButtons.tsx b/src/features/wallet/WalletFloatingButtons.tsx
deleted file mode 100644
index e93093e9..00000000
--- a/src/features/wallet/WalletFloatingButtons.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import Link from 'next/link';
-
-import { DocsIcon, HistoryIcon, IconButton } from '@hyperlane-xyz/widgets';
-import { links } from '../../consts/links';
-import { Color } from '../../styles/Color';
-import { useStore } from '../store';
-
-export function WalletFloatingButtons() {
- const { setIsSideBarOpen, isSideBarOpen } = useStore((s) => ({
- setIsSideBarOpen: s.setIsSideBarOpen,
- isSideBarOpen: s.isSideBarOpen,
- }));
-
- return (
-
- setIsSideBarOpen(!isSideBarOpen)}
- >
-
-
-
-
-
-
- );
-}
-
-const styles = {
- link: 'hover:opacity-70 active:opacity-60',
- roundedCircle: 'rounded-full bg-white',
-};
diff --git a/src/features/warpCore/AddWarpConfigModal.tsx b/src/features/warpCore/AddWarpConfigModal.tsx
new file mode 100644
index 00000000..697662c6
--- /dev/null
+++ b/src/features/warpCore/AddWarpConfigModal.tsx
@@ -0,0 +1,170 @@
+import { warpRouteConfigToId } from '@hyperlane-xyz/registry';
+import { MultiProtocolProvider, WarpCoreConfig, WarpCoreConfigSchema } from '@hyperlane-xyz/sdk';
+import { failure, Result, success, tryParseJsonOrYaml } from '@hyperlane-xyz/utils';
+import { Button, CopyButton, IconButton, Modal, PlusIcon, XIcon } from '@hyperlane-xyz/widgets';
+import clsx from 'clsx';
+import { useState } from 'react';
+import { toast } from 'react-toastify';
+import { Color } from '../../styles/Color';
+import { logger } from '../../utils/logger';
+import { useMultiProvider } from '../chains/hooks';
+import { useStore } from '../store';
+
+export function AddWarpConfigModal({ isOpen, close }: { isOpen: boolean; close: () => void }) {
+ const { warpCoreConfigOverrides, setWarpCoreConfigOverrides } = useStore(
+ ({ warpCoreConfigOverrides, setWarpCoreConfigOverrides }) => ({
+ warpCoreConfigOverrides,
+ setWarpCoreConfigOverrides,
+ }),
+ );
+
+ const onAddConfig = (warpCoreConfig: WarpCoreConfig) => {
+ setWarpCoreConfigOverrides([...warpCoreConfigOverrides, warpCoreConfig]);
+ toast.success('Warp config added!');
+ close();
+ };
+
+ const onRemoveConfig = (index: number) => {
+ setWarpCoreConfigOverrides(warpCoreConfigOverrides.filter((_, i) => i !== index));
+ toast.success('Warp config removed');
+ };
+
+ return (
+
+ Add Warp Route Configs
+
+ Add warp route configs, like those from the Hyperlane CLI. Note, these routes will be
+ available only in your own browser.
+
+
+
+
+ );
+}
+
+// TODO de-dupe with Form in ChainAddMenu in widgets lib
+function Form({ onAdd }: { onAdd: (warpCoreConfig: WarpCoreConfig) => void }) {
+ const multiProvider = useMultiProvider();
+ const [textInput, setTextInput] = useState('');
+ const [error, setError] = useState(null);
+
+ const onChangeInput = (e: React.ChangeEvent) => {
+ setTextInput(e.target.value);
+ setError(null);
+ };
+
+ const onClickAdd = () => {
+ const result = tryParseConfigInput(textInput, multiProvider);
+ if (result.success) {
+ onAdd(result.data);
+ } else {
+ setError(`Invalid config: ${result.error}`);
+ }
+ };
+
+ return (
+ <>
+
+
+ {error &&
{error}
}
+
+
+
+ >
+ );
+}
+
+function ConfigList({
+ warpCoreConfigOverrides,
+ onRemove,
+}: {
+ warpCoreConfigOverrides: WarpCoreConfig[];
+ onRemove: (index: number) => void;
+}) {
+ if (!warpCoreConfigOverrides.length) return null;
+
+ return (
+
+ {warpCoreConfigOverrides.map((config, i) => (
+
+ {warpRouteConfigToId(config)}
+ onRemove(i)} title="Remove config">
+
+
+
+ ))}
+
+ );
+}
+
+function tryParseConfigInput(
+ input: string,
+ multiProvider: MultiProtocolProvider,
+): Result {
+ const parsed = tryParseJsonOrYaml(input);
+ if (!parsed.success) return parsed;
+
+ const result = WarpCoreConfigSchema.safeParse(parsed.data);
+
+ if (!result.success) {
+ logger.warn('Error validating warp config', result.error);
+ const firstIssue = result.error.issues[0];
+ return failure(`${firstIssue.path} => ${firstIssue.message}`);
+ }
+
+ const warpConfig = result.data;
+ const warpChains = warpConfig.tokens.map((t) => t.chainName);
+ const unknownChain = warpChains.find((c) => !multiProvider.hasChain(c));
+
+ if (unknownChain) {
+ return failure(`Unknown chain: ${unknownChain}`);
+ }
+
+ return success(result.data);
+}
+
+const placeholderText = `# YAML config data
+---
+tokens:
+ - addressOrDenom: "0x123..."
+ chainName: ethereum
+ collateralAddressOrDenom: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
+ connections:
+ - token: ethereum|mycoolchain|0x345...
+ decimals: 6
+ name: USDC
+ standard: EvmHypCollateral
+ symbol: USDC
+ - addressOrDenom: "0x345..."
+ chainName: mycoolchain
+ connections:
+ - token: ethereum|ethereum|0x123...
+ decimals: 6
+ name: USDC
+ standard: EvmHypSynthetic
+ symbol: USDC
+options: {}
+`;
diff --git a/src/features/warpCore/warpCoreConfig.ts b/src/features/warpCore/warpCoreConfig.ts
new file mode 100644
index 00000000..d77f6b8c
--- /dev/null
+++ b/src/features/warpCore/warpCoreConfig.ts
@@ -0,0 +1,75 @@
+import { warpRouteConfigs as registryWarpRoutes } from '@hyperlane-xyz/registry';
+import { WarpCoreConfig, WarpCoreConfigSchema, validateZodResult } from '@hyperlane-xyz/sdk';
+import { objFilter, objMerge } from '@hyperlane-xyz/utils';
+import { warpRouteWhitelist } from '../../consts/warpRouteWhitelist.ts';
+import { warpRouteConfigs as tsWarpRoutes } from '../../consts/warpRoutes.ts';
+import yamlWarpRoutes from '../../consts/warpRoutes.yaml';
+
+export function assembleWarpCoreConfig(storeOverrides: WarpCoreConfig[]): WarpCoreConfig {
+ const yamlResult = WarpCoreConfigSchema.safeParse(yamlWarpRoutes);
+ const yamlConfig = validateZodResult(yamlResult, 'warp core yaml config');
+ const tsResult = WarpCoreConfigSchema.safeParse(tsWarpRoutes);
+ const tsConfig = validateZodResult(tsResult, 'warp core typescript config');
+
+ const filteredRegistryConfigMap = warpRouteWhitelist
+ ? filterToIds(registryWarpRoutes, warpRouteWhitelist)
+ : registryWarpRoutes;
+ const filteredRegistryConfigValues = Object.values(filteredRegistryConfigMap);
+ const filteredRegistryTokens = filteredRegistryConfigValues.map((c) => c.tokens).flat();
+ const filteredRegistryOptions = filteredRegistryConfigValues.map((c) => c.options).flat();
+
+ const storeOverrideTokens = storeOverrides.map((c) => c.tokens).flat();
+ const storeOverrideOptions = storeOverrides.map((c) => c.options).flat();
+
+ const combinedTokens = [
+ ...filteredRegistryTokens,
+ ...tsConfig.tokens,
+ ...yamlConfig.tokens,
+ ...storeOverrideTokens,
+ ];
+ const tokens = dedupeTokens(combinedTokens);
+
+ const combinedOptions = [
+ ...filteredRegistryOptions,
+ tsConfig.options,
+ yamlConfig.options,
+ ...storeOverrideOptions,
+ ];
+ const options = reduceOptions(combinedOptions);
+
+ if (!tokens.length)
+ throw new Error(
+ 'No warp route configs provided. Please check your registry, warp route whitelist, and custom route configs for issues.',
+ );
+
+ return { tokens, options };
+}
+
+function filterToIds(
+ config: Record,
+ idWhitelist: string[],
+): Record {
+ return objFilter(config, (id, c): c is WarpCoreConfig => idWhitelist.includes(id));
+}
+
+// Separate warp configs may contain duplicate definitions of the same token.
+// E.g. an IBC token that gets used for interchain gas in many different routes.
+function dedupeTokens(tokens: WarpCoreConfig['tokens']): WarpCoreConfig['tokens'] {
+ const idToToken: Record = {};
+ for (const token of tokens) {
+ const id = `${token.chainName}|${token.addressOrDenom?.toLowerCase()}`;
+ idToToken[id] = objMerge(idToToken[id] || {}, token);
+ }
+ return Object.values(idToToken);
+}
+
+// Combine a list of WarpCore option objects into one single options object
+function reduceOptions(optionsList: Array): WarpCoreConfig['options'] {
+ return optionsList.reduce((acc, o) => {
+ if (!o || !acc) return acc;
+ for (const key of Object.keys(o)) {
+ acc[key] = (acc[key] || []).concat(o[key] || []);
+ }
+ return acc;
+ }, {});
+}
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 3fb706aa..0b02979a 100755
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,7 +1,7 @@
import type { NextPage } from 'next';
+import { FloatingButtonStrip } from '../components/nav/FloatingButtonStrip';
import { TipCard } from '../components/tip/TipCard';
import { TransferTokenCard } from '../features/transfer/TransferTokenCard';
-import { WalletFloatingButtons } from '../features/wallet/WalletFloatingButtons';
const Home: NextPage = () => {
return (
@@ -9,7 +9,7 @@ const Home: NextPage = () => {
-
+