Skip to content

Commit

Permalink
feat: Replace chain picker with new ChainSearchMenu from Widgets lib (#…
Browse files Browse the repository at this point in the history
…328)

Use Widgets lib's ChainSearchMenu but with a custom field of the number of routes for the oppositely selected chain

---------

Co-authored-by: Xaroz <[email protected]>
  • Loading branch information
jmrossy and Xaroz authored Nov 19, 2024
1 parent bb3982f commit 48776d5
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 40 deletions.
8 changes: 4 additions & 4 deletions src/features/chains/ChainSelectField.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChevronIcon } from '@hyperlane-xyz/widgets';
import { ChainSearchMenuProps, ChevronIcon } from '@hyperlane-xyz/widgets';
import { useField, useFormikContext } from 'formik';
import { useState } from 'react';
import { ChainLogo } from '../../components/icons/ChainLogo';
Expand All @@ -9,12 +9,12 @@ import { useChainDisplayName } from './hooks';
type Props = {
name: string;
label: string;
chains: ChainName[];
onChange?: (id: ChainName) => void;
disabled?: boolean;
customListItemField: ChainSearchMenuProps['customListItemField'];
};

export function ChainSelectField({ name, label, chains, onChange, disabled }: Props) {
export function ChainSelectField({ name, label, onChange, disabled, customListItemField }: Props) {
const [field, , helpers] = useField<ChainName>(name);
const { setFieldValue } = useFormikContext<TransferFormValues>();

Expand Down Expand Up @@ -59,8 +59,8 @@ export function ChainSelectField({ name, label, chains, onChange, disabled }: Pr
<ChainSelectListModal
isOpen={isModalOpen}
close={() => setIsModalOpen(false)}
chains={chains}
onSelect={handleChange}
customListItemField={customListItemField}
/>
</div>
);
Expand Down
52 changes: 21 additions & 31 deletions src/features/chains/ChainSelectModal.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,41 @@
import { Modal } from '@hyperlane-xyz/widgets';
import { useMemo } from 'react';
import { ChainLogo } from '../../components/icons/ChainLogo';
import { ChainMetadata } from '@hyperlane-xyz/sdk';
import { ChainSearchMenu, ChainSearchMenuProps, Modal } from '@hyperlane-xyz/widgets';
import { useStore } from '../store';
import { useMultiProvider } from './hooks';
import { getChainDisplayName } from './utils';

export function ChainSelectListModal({
isOpen,
close,
chains,
onSelect,
customListItemField,
}: {
isOpen: boolean;
close: () => void;
chains: ChainName[];
onSelect: (chain: ChainName) => void;
customListItemField: ChainSearchMenuProps['customListItemField'];
}) {
const multiProvider = useMultiProvider();

const sortedChains = useMemo(() => chains.sort(), [chains]);
const { chainMetadataOverrides, setChainMetadataOverrides } = useStore((s) => ({
chainMetadataOverrides: s.chainMetadataOverrides,
setChainMetadataOverrides: s.setChainMetadataOverrides,
}));

const onSelectChain = (chain: ChainName) => {
return () => {
onSelect(chain);
close();
};
const onSelectChain = (chain: ChainMetadata) => {
onSelect(chain.name);
close();
};

return (
<Modal
isOpen={isOpen}
close={close}
title="Select Chain"
panelClassname="p-4 max-w-xs"
showCloseButton
>
<div className="mt-2 flex flex-col space-y-1">
{sortedChains.map((c) => (
<button
key={c}
className="flex items-center rounded px-2 py-1.5 text-sm transition-all duration-200 hover:bg-gray-100 active:bg-gray-200"
onClick={onSelectChain(c)}
>
<ChainLogo chainName={c} size={16} background={false} />
<span className="ml-2">{getChainDisplayName(multiProvider, c, true)}</span>
</button>
))}
</div>
<Modal isOpen={isOpen} close={close} panelClassname="p-4 sm:p-5 max-w-lg min-h-[40vh]">
<ChainSearchMenu
chainMetadata={multiProvider.metadata}
onClickChain={onSelectChain}
overrideChainMetadata={chainMetadataOverrides}
onChangeOverrideMetadata={setChainMetadataOverrides}
customListItemField={customListItemField}
defaultSortField="custom"
/>
</Modal>
);
}
43 changes: 41 additions & 2 deletions src/features/chains/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isAbacusWorksChain } from '@hyperlane-xyz/registry';
import { MultiProtocolProvider } from '@hyperlane-xyz/sdk';
import { ProtocolType, toTitleCase } from '@hyperlane-xyz/utils';
import { ChainMap, MultiProtocolProvider, WarpCore } from '@hyperlane-xyz/sdk';
import { ProtocolType, toTitleCase, trimToLength } from '@hyperlane-xyz/utils';
import { ChainSearchMenuProps } from '@hyperlane-xyz/widgets';

export function getChainDisplayName(
multiProvider: MultiProtocolProvider,
Expand Down Expand Up @@ -31,3 +32,41 @@ export function getChainByRpcUrl(multiProvider: MultiProtocolProvider, url?: str
(m) => !!m.rpcUrls.find((rpc) => rpc.http.toLowerCase().includes(url.toLowerCase())),
);
}

/**
* Returns an object that contains the amount of
* routes from a single chain to every other chain
*/
export function getNumRoutesWithSelectedChain(
warpCore: WarpCore,
selectedChain: ChainName,
isSelectedChainOrigin: boolean,
): ChainSearchMenuProps['customListItemField'] {
const multiProvider = warpCore.multiProvider;
const chains = multiProvider.metadata;
const selectedChainDisplayName = trimToLength(
getChainDisplayName(multiProvider, selectedChain, true),
10,
);

const data = Object.keys(chains).reduce<ChainMap<{ display: string; sortValue: number }>>(
(result, otherChain) => {
const origin = isSelectedChainOrigin ? selectedChain : otherChain;
const destination = isSelectedChainOrigin ? otherChain : selectedChain;
const tokens = warpCore.getTokensForRoute(origin, destination).length;
result[otherChain] = {
display: `${tokens} route${tokens > 1 ? 's' : ''}`,
sortValue: tokens,
};

return result;
},
{},
);

const preposition = isSelectedChainOrigin ? 'from' : 'to';
return {
header: `Routes ${preposition} ${selectedChainDisplayName}`,
data,
};
}
26 changes: 23 additions & 3 deletions src/features/transfer/TransferTokenForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { logger } from '../../utils/logger';
import { ChainSelectField } from '../chains/ChainSelectField';
import { ChainWalletWarning } from '../chains/ChainWalletWarning';
import { useChainDisplayName, useMultiProvider } from '../chains/hooks';
import { getNumRoutesWithSelectedChain } from '../chains/utils';
import { useIsAccountSanctioned } from '../sanctions/hooks/useIsAccountSanctioned';
import { useStore } from '../store';
import { SelectOrInputTokenIds } from '../tokens/SelectOrInputTokenIds';
Expand Down Expand Up @@ -113,15 +114,34 @@ function SwapChainsButton({ disabled }: { disabled?: boolean }) {

function ChainSelectSection({ isReview }: { isReview: boolean }) {
const warpCore = useWarpCore();
const chains = useMemo(() => warpCore.getTokenChains(), [warpCore]);

const { values } = useFormikContext<TransferFormValues>();

const originRouteCounts = useMemo(() => {
return getNumRoutesWithSelectedChain(warpCore, values.origin, true);
}, [values.origin, warpCore]);

const destinationRouteCounts = useMemo(() => {
return getNumRoutesWithSelectedChain(warpCore, values.destination, false);
}, [values.destination, warpCore]);

return (
<div className="mt-4 flex items-center justify-between gap-4">
<ChainSelectField name="origin" label="From" chains={chains} disabled={isReview} />
<ChainSelectField
name="origin"
label="From"
disabled={isReview}
customListItemField={destinationRouteCounts}
/>
<div className="flex flex-1 flex-col items-center">
<SwapChainsButton disabled={isReview} />
</div>
<ChainSelectField name="destination" label="To" chains={chains} disabled={isReview} />
<ChainSelectField
name="destination"
label="To"
disabled={isReview}
customListItemField={originRouteCounts}
/>
</div>
);
}
Expand Down

0 comments on commit 48776d5

Please sign in to comment.