Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Main to trump #405

Merged
merged 15 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 8 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@hyperlane-xyz/warp-ui-template",
"description": "A web app template for building Hyperlane Warp Route UIs",
"version": "7.1.0",
"version": "8.5.0",
"author": "J M Rossy",
"dependencies": {
"@chakra-ui/next-js": "^2.4.2",
Expand All @@ -17,10 +17,10 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@headlessui/react": "^2.2.0",
"@hyperlane-xyz/registry": "6.19.1",
"@hyperlane-xyz/sdk": "7.1.0",
"@hyperlane-xyz/utils": "7.1.0",
"@hyperlane-xyz/widgets": "7.1.0",
"@hyperlane-xyz/registry": "7.2.2",
"@hyperlane-xyz/sdk": "8.5.0",
"@hyperlane-xyz/utils": "8.5.0",
"@hyperlane-xyz/widgets": "8.5.0",
"@interchain-ui/react": "^1.23.28",
"@metamask/post-message-stream": "6.1.2",
"@metamask/providers": "10.2.1",
Expand Down Expand Up @@ -69,6 +69,8 @@
"tailwindcss": "^3.4.15",
"ts-node": "^10.9.2",
"typescript": "5.6.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.3",
"yaml": "^2.6.0",
"yaml-loader": "^0.8.1"
},
Expand All @@ -88,7 +90,7 @@
"typecheck": "tsc",
"lint": "next lint",
"start": "next start",
"test": "echo 'No tests'",
"test": "vitest --watch false",
"prettier": "prettier --write ./src"
},
"types": "dist/src/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/components/buttons/ConnectAwareSubmitButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function ConnectAwareSubmitButton<FormValues = any>({ chainName, text, cl
useTimeout(clearErrors, 3500);

return (
<SolidButton type={type} color={color} onClick={onClick} classes={classes}>
<SolidButton type={type} color={color} onClick={onClick} className={classes}>
{content}
</SolidButton>
);
Expand Down
6 changes: 3 additions & 3 deletions src/components/buttons/SolidButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ interface ButtonProps {
type?: 'submit' | 'reset' | 'button';
color?: 'white' | 'primary' | 'accent' | 'green' | 'red' | 'gray'; // defaults to primary
bold?: boolean;
classes?: string;
className?: string;
icon?: ReactElement;
}

Expand All @@ -15,7 +15,7 @@ export function SolidButton(
type,
onClick,
color: _color,
classes,
className,
bold,
icon,
disabled,
Expand Down Expand Up @@ -48,7 +48,7 @@ export function SolidButton(
}
const onDisabled = 'disabled:bg-gray-300 disabled:text-gray-500';
const weight = bold ? 'font-semibold' : '';
const allClasses = `${base} ${baseColors} ${onHover} ${onDisabled} ${weight} ${classes}`;
const allClasses = `${base} ${baseColors} ${onHover} ${onDisabled} ${weight} ${className}`;

return (
<button
Expand Down
5 changes: 5 additions & 0 deletions src/consts/args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum WARP_QUERY_PARAMS {
ORIGIN = 'origin',
DESTINATION = 'destination',
TOKEN = 'token',
}
10 changes: 10 additions & 0 deletions src/consts/warpRouteWhitelist.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { warpRouteConfigs } from '@hyperlane-xyz/registry';
import { assert, test } from 'vitest';
import { warpRouteWhitelist } from './warpRouteWhitelist';

test('warpRouteWhitelist', () => {
if (!warpRouteWhitelist) return;
for (const id of warpRouteWhitelist) {
assert(warpRouteConfigs[id], `No route with id ${id} found in registry.`);
}
});
5 changes: 2 additions & 3 deletions src/features/chains/ChainSelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useChainDisplayName } from './hooks';
type Props = {
name: string;
label: string;
onChange?: (id: ChainName) => void;
onChange?: (id: ChainName, fieldName: string) => void;
disabled?: boolean;
customListItemField: ChainSearchMenuProps['customListItemField'];
};
Expand All @@ -25,8 +25,7 @@ export function ChainSelectField({ name, label, onChange, disabled, customListIt
// Reset other fields on chain change
setFieldValue('recipient', '');
setFieldValue('amount', '');
setFieldValue('tokenIndex', undefined);
if (onChange) onChange(chainName);
if (onChange) onChange(chainName, name);
};

const [isModalOpen, setIsModalOpen] = useState(false);
Expand Down
10 changes: 10 additions & 0 deletions src/features/chains/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,13 @@ export function getNumRoutesWithSelectedChain(
data,
};
}

/**
* Return given chainName if it is valid, otherwise return undefined
*/
export function tryGetValidChainName(
chainName: string | null,
multiProvider: MultiProtocolProvider,
): string | undefined {
return chainName && multiProvider.tryGetChainName(chainName) ? chainName : undefined;
}
24 changes: 5 additions & 19 deletions src/features/tokens/TokenSelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ChevronIcon } from '@hyperlane-xyz/widgets';
import { useField, useFormikContext } from 'formik';
import { useEffect, useState } from 'react';
import { TokenIcon } from '../../components/icons/TokenIcon';

import { TransferFormValues } from '../transfer/types';
import { TokenListModal } from './TokenListModal';
import { getIndexForToken, getTokenByIndex, useWarpCore } from './hooks';
Expand All @@ -11,9 +12,10 @@ type Props = {
name: string;
disabled?: boolean;
setIsNft: (value: boolean) => void;
onChangeToken: (addressOrDenom: string) => void;
};

export function TokenSelectField({ name, disabled, setIsNft }: Props) {
export function TokenSelectField({ name, disabled, setIsNft, onChangeToken }: Props) {
const { values } = useFormikContext<TransferFormValues>();
const [field, , helpers] = useField<number | undefined>(name);
const [isModalOpen, setIsModalOpen] = useState(false);
Expand All @@ -24,29 +26,13 @@ export function TokenSelectField({ name, disabled, setIsNft }: Props) {
const { origin, destination } = values;
useEffect(() => {
const tokensWithRoute = warpCore.getTokensForRoute(origin, destination);
let newFieldValue: number | undefined;
let newIsAutomatic: boolean;
// No tokens available for this route
if (tokensWithRoute.length === 0) {
newFieldValue = undefined;
newIsAutomatic = true;
}
// Exactly one found
else if (tokensWithRoute.length === 1) {
newFieldValue = getIndexForToken(warpCore, tokensWithRoute[0]);
newIsAutomatic = true;
// Multiple possibilities
} else {
newFieldValue = undefined;
newIsAutomatic = false;
}
helpers.setValue(newFieldValue);
setIsAutomaticSelection(newIsAutomatic);
setIsAutomaticSelection(tokensWithRoute.length <= 1);
}, [warpCore, origin, destination, helpers]);

const onSelectToken = (newToken: IToken) => {
// Set the token address value in formik state
helpers.setValue(getIndexForToken(warpCore, newToken));
onChangeToken(newToken.addressOrDenom);
// Update nft state in parent
setIsNft(newToken.isNft());
};
Expand Down
22 changes: 21 additions & 1 deletion src/features/tokens/balances.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { IToken } from '@hyperlane-xyz/sdk';
import { IToken, MultiProtocolProvider, Token } from '@hyperlane-xyz/sdk';
import { isValidAddress } from '@hyperlane-xyz/utils';
import { useAccountAddressForChain } from '@hyperlane-xyz/widgets';
import { useQuery } from '@tanstack/react-query';
import { toast } from 'react-toastify';
import { useToastError } from '../../components/toast/useToastError';
import { logger } from '../../utils/logger';
import { useMultiProvider } from '../chains/hooks';
import { getChainDisplayName } from '../chains/utils';
import { TransferFormValues } from '../transfer/types';
import { useTokenByIndex } from './hooks';

Expand Down Expand Up @@ -41,3 +44,20 @@ export function useDestinationBalance({ destination, tokenIndex, recipient }: Tr
const connection = originToken?.getConnectionForChain(destination);
return useBalance(destination, connection?.token, recipient);
}

export async function getDestinationNativeBalance(
multiProvider: MultiProtocolProvider,
{ destination, recipient }: TransferFormValues,
) {
try {
const chainMetadata = multiProvider.getChainMetadata(destination);
const token = Token.FromChainMetadataNativeToken(chainMetadata);
const balance = await token.getBalance(multiProvider, recipient);
return balance.amount;
} catch (error) {
const msg = `Error checking recipient balance on ${getChainDisplayName(multiProvider, destination)}`;
logger.error(msg, error);
toast.error(msg);
return undefined;
}
}
44 changes: 44 additions & 0 deletions src/features/tokens/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,47 @@ export function tryFindToken(
return null;
}
}

function getTokenIndexFromChains(
warpCore: WarpCore,
addressOrDenom: string | null,
origin: string,
destination: string,
) {
// find routes
const tokensWithRoute = warpCore.getTokensForRoute(origin, destination);
// find provided token addressOrDenom
const queryToken = tokensWithRoute.find((token) => token.addressOrDenom === addressOrDenom);

// if found return index
if (queryToken) return getIndexForToken(warpCore, queryToken);
// if tokens route has only one route return that index
else if (tokensWithRoute.length === 1) return getIndexForToken(warpCore, tokensWithRoute[0]);
// if 0 or more than 1 then return undefined
return undefined;
}

export function getInitialTokenIndex(
warpCore: WarpCore,
addressOrDenom: string | null,
originQuery?: string,
destinationQuery?: string,
): number | undefined {
const firstToken = warpCore.tokens[0];
const connectedToken = firstToken.connections?.[0];

// origin query and destination query is defined
if (originQuery && destinationQuery)
return getTokenIndexFromChains(warpCore, addressOrDenom, originQuery, destinationQuery);

// if none of those are defined, use default values and pass token query
if (connectedToken)
return getTokenIndexFromChains(
warpCore,
addressOrDenom,
firstToken.chainName,
connectedToken.token.chainName,
);

return undefined;
}
44 changes: 44 additions & 0 deletions src/features/transfer/RecipientConfirmationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Modal } from '@hyperlane-xyz/widgets';
import { useFormikContext } from 'formik';
import { SolidButton } from '../../components/buttons/SolidButton';
import { TransferFormValues } from './types';

export function RecipientConfirmationModal({
isOpen,
close,
onConfirm,
}: {
isOpen: boolean;
close: () => void;
onConfirm: () => void;
}) {
const { values } = useFormikContext<TransferFormValues>();
return (
<Modal
isOpen={isOpen}
close={close}
title="Confirm Recipient Address"
panelClassname="flex flex-col items-center p-4 gap-5"
>
<p className="text-center text-sm">
The recipient address has no funds on the destination chain. Is this address correct?
</p>
<p className="rounded-lg bg-primary-500/5 p-2 text-center text-sm">{values.recipient}</p>
<div className="flex items-center justify-center gap-12">
<SolidButton onClick={close} color="gray" className="min-w-24 px-4 py-1">
Cancel
</SolidButton>
<SolidButton
onClick={() => {
close();
onConfirm();
}}
color="primary"
className="min-w-24 px-4 py-1"
>
Continue
</SolidButton>
</div>
</Modal>
);
}
Loading