Skip to content

Commit

Permalink
refactor: Replace global warp context with store data (#319)
Browse files Browse the repository at this point in the history
Move the Warp Context pieces (warpCore, multiProvider, registry) into the Zustand store. This enables support for dynamic chain metadata / token route overrides at runtime, as required for the integration of the new ChainSearchMenu component from the widgets lib.
  • Loading branch information
jmrossy authored Nov 11, 2024
1 parent b8f74d2 commit 55e985a
Show file tree
Hide file tree
Showing 40 changed files with 537 additions and 360 deletions.
4 changes: 2 additions & 2 deletions src/components/buttons/ConnectAwareSubmitButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ProtocolType } from '@hyperlane-xyz/utils';
import { useFormikContext } from 'formik';
import { useCallback } from 'react';
import { tryGetChainProtocol } from '../../features/chains/utils';
import { useChainProtocol } from '../../features/chains/hooks';
import { useAccountForChain, useConnectFns } from '../../features/wallet/hooks/multiProtocol';
import { useTimeout } from '../../utils/timeout';
import { SolidButton } from './SolidButton';
Expand All @@ -13,7 +13,7 @@ interface Props {
}

export function ConnectAwareSubmitButton<FormValues = any>({ chainName, text, classes }: Props) {
const protocol = tryGetChainProtocol(chainName) || ProtocolType.Ethereum;
const protocol = useChainProtocol(chainName) || ProtocolType.Ethereum;
const connectFns = useConnectFns();
const connectFn = connectFns[protocol];

Expand Down
2 changes: 1 addition & 1 deletion src/components/errors/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class ErrorBoundary extends Component<any, ErrorBoundaryState> {
<div className="flex flex-col items-center">
<Image src={ErrorIcon} width={80} height={80} alt="" />
<h1 className="mt-5 text-lg">Fatal Error Occurred</h1>
<div className="mt-5 text-sm">{details}</div>
<div className="mt-5 max-w-2xl text-sm">{details}</div>
<a
href={links.discord}
target="_blank"
Expand Down
11 changes: 5 additions & 6 deletions src/components/icons/ChainLogo.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ChainLogo as ChainLogoInner } from '@hyperlane-xyz/widgets';
import Image from 'next/image';
import { useMemo } from 'react';
import { getRegistry } from '../../context/context';
import { tryGetChainMetadata } from '../../features/chains/utils';
import { useChainMetadata } from '../../features/chains/hooks';
import { useStore } from '../../features/store';

export function ChainLogo({
chainName,
Expand All @@ -13,10 +13,9 @@ export function ChainLogo({
background?: boolean;
size?: number;
}) {
const registry = getRegistry();
const registry = useStore((s) => s.registry);
const chainMetadata = useChainMetadata(chainName);
const { name, Icon } = useMemo(() => {
if (!chainName) return { name: '' };
const chainMetadata = tryGetChainMetadata(chainName);
const name = chainMetadata?.name || '';
const logoUri = chainMetadata?.logoURI;
const Icon = logoUri
Expand All @@ -28,7 +27,7 @@ export function ChainLogo({
name,
Icon,
};
}, [chainName]);
}, [chainMetadata]);

return (
<ChainLogoInner
Expand Down
15 changes: 7 additions & 8 deletions src/components/icons/TokenIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IRegistry } from '@hyperlane-xyz/registry';
import { IToken } from '@hyperlane-xyz/sdk';
import { Circle } from '@hyperlane-xyz/widgets';
import { memo } from 'react';
import { getRegistry } from '../../context/context';
import { useStore } from '../../features/store';
import { isValidHttpsUrl, isValidRelativeUrl } from '../../utils/url';
import { ErrorBoundary } from '../errors/ErrorBoundary';

Expand All @@ -10,12 +10,13 @@ interface Props {
size?: number;
}

function _TokenIcon({ token, size = 32 }: Props) {
export function TokenIcon({ token, size = 32 }: Props) {
const title = token?.symbol || '';
const character = title ? title.charAt(0).toUpperCase() : '';
const fontSize = Math.floor(size / 2);

const imageSrc = getImageSrc(token);
const registry = useStore((s) => s.registry);
const imageSrc = getImageSrc(registry, token);
const bgColorSeed =
token && !imageSrc ? (Buffer.from(token.addressOrDenom).at(0) || 0) % 5 : undefined;

Expand All @@ -32,13 +33,11 @@ function _TokenIcon({ token, size = 32 }: Props) {
);
}

function getImageSrc(token?: IToken | null) {
function getImageSrc(registry: IRegistry, token?: IToken | null) {
if (!token?.logoURI) return null;
// If it's a valid, direct URL, return it
if (isValidHttpsUrl(token.logoURI)) return token.logoURI;
// Otherwise assume it's a relative URL to the registry base
if (isValidRelativeUrl(token.logoURI)) return getRegistry().getUri(token.logoURI);
if (isValidRelativeUrl(token.logoURI)) return registry.getUri(token.logoURI);
return null;
}

export const TokenIcon = memo(_TokenIcon);
8 changes: 3 additions & 5 deletions src/components/toast/TxSuccessToast.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useMemo } from 'react';
import { toast } from 'react-toastify';
import { getMultiProvider } from '../../context/context';
import { useMultiProvider } from '../../features/chains/hooks';

export function toastTxSuccess(msg: string, txHash: string, chain: ChainName) {
toast.success(<TxSuccessToast msg={msg} txHash={txHash} chain={chain} />, {
Expand All @@ -17,9 +16,8 @@ export function TxSuccessToast({
txHash: string;
chain: ChainName;
}) {
const url = useMemo(() => {
return getMultiProvider().tryGetExplorerTxUrl(chain, { hash: txHash });
}, [chain, txHash]);
const multiProvider = useMultiProvider();
const url = multiProvider.tryGetExplorerTxUrl(chain, { hash: txHash });

return (
<div>
Expand Down
1 change: 0 additions & 1 deletion src/consts/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,3 @@ export const APP_URL = 'hyperlane-warp-template.vercel.app';
export const BRAND_COLOR = Color.primary;
export const BACKGROUND_COLOR = Color.primary;
export const BACKGROUND_IMAGE = 'url(/backgrounds/main.svg)';
export const PROXY_DEPLOYED_URL = 'https://proxy.hyperlane.xyz';
3 changes: 3 additions & 0 deletions src/consts/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ADDRESS_BLACKLIST } from './blacklist';
const isDevMode = process?.env?.NODE_ENV === 'development';
const version = process?.env?.NEXT_PUBLIC_VERSION || '0.0.0';
const registryUrl = process?.env?.NEXT_PUBLIC_REGISTRY_URL || undefined;
const registryProxyUrl = process?.env?.NEXT_PUBLIC_GITHUB_PROXY || 'https://proxy.hyperlane.xyz';
const explorerApiKeys = JSON.parse(process?.env?.EXPLORER_API_KEYS || '{}');
const walletConnectProjectId = process?.env?.NEXT_PUBLIC_WALLET_CONNECT_ID || '';
const withdrawalWhitelist = process?.env?.NEXT_PUBLIC_BLOCK_WITHDRAWAL_WHITELIST || '';
Expand All @@ -17,6 +18,7 @@ interface Config {
explorerApiKeys: Record<string, string>; // Optional map of API keys for block explorer
isDevMode: boolean; // Enables some debug features in the app
registryUrl: string | undefined; // Optional URL to use a custom registry instead of the published canonical version
registryProxyUrl?: string; // Optional URL to use a custom proxy for the GithubRegistry
showDisabledTokens: boolean; // Show/Hide invalid token options in the selection modal
showTipBox: boolean; // Show/Hide the blue tip box above the transfer form
transferBlacklist: string; // comma-separated list of routes between which transfers are disabled. Expects Caip2Id-Caip2Id (e.g. ethereum:1-sealevel:1399811149)
Expand All @@ -32,6 +34,7 @@ export const config: Config = Object.freeze({
explorerApiKeys,
isDevMode,
registryUrl,
registryProxyUrl,
showDisabledTokens: true,
showTipBox: true,
version,
Expand Down
38 changes: 0 additions & 38 deletions src/context/WarpContext.tsx

This file was deleted.

41 changes: 0 additions & 41 deletions src/context/chains.ts

This file was deleted.

73 changes: 0 additions & 73 deletions src/context/context.ts

This file was deleted.

31 changes: 31 additions & 0 deletions src/features/WarpContextInitGate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { PropsWithChildren, useState } from 'react';
import { Spinner } from '../components/animation/Spinner';
import { useTimeout } from '../utils/timeout';
import { useReadyMultiProvider } from './chains/hooks';

const INIT_TIMEOUT = 10_000; // 10 seconds

// A wrapper app to delay rendering children until the warp context is ready
export function WarpContextInitGate({ children }: PropsWithChildren<unknown>) {
const isWarpContextReady = !!useReadyMultiProvider();

const [isTimedOut, setIsTimedOut] = useState(false);
useTimeout(() => setIsTimedOut(true), INIT_TIMEOUT);

if (!isWarpContextReady) {
if (isTimedOut) {
// Fallback to outer error boundary
throw new Error(
'Failed to initialize warp context. Please check your registry URL and connection status.',
);
} else {
return (
<div className="flex h-screen items-center justify-center bg-primary-500">
<Spinner classes="opacity-50" white />
</div>
);
}
}

return <>{children}</>;
}
6 changes: 4 additions & 2 deletions src/features/chains/ChainSelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ChainLogo } from '../../components/icons/ChainLogo';
import ChevronIcon from '../../images/icons/chevron-down.svg';
import { TransferFormValues } from '../transfer/types';
import { ChainSelectListModal } from './ChainSelectModal';
import { getChainDisplayName } from './utils';
import { useChainDisplayName } from './hooks';

type Props = {
name: string;
Expand All @@ -19,6 +19,8 @@ export function ChainSelectField({ name, label, chains, onChange, disabled }: Pr
const [field, , helpers] = useField<ChainName>(name);
const { setFieldValue } = useFormikContext<TransferFormValues>();

const displayName = useChainDisplayName(field.value, true);

const handleChange = (chainName: ChainName) => {
helpers.setValue(chainName);
// Reset other fields on chain change
Expand Down Expand Up @@ -50,7 +52,7 @@ export function ChainSelectField({ name, label, chains, onChange, disabled }: Pr
<label htmlFor={name} className="text-xs text-gray-600">
{label}
</label>
{getChainDisplayName(field.value, true)}
{displayName}
</div>
</div>
<Image src={ChevronIcon} width={12} height={8} alt="" />
Expand Down
Loading

0 comments on commit 55e985a

Please sign in to comment.