diff --git a/.eslintignore b/.eslintignore
index 679839c..a72b3c4 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -2,5 +2,6 @@ node_modules
dist
build
coverage
+postcss.config.js
next.config.js
tailwind.config.js
\ No newline at end of file
diff --git a/README.md b/README.md
index 0859afb..b9453bd 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ An interchain web app for bridging tokens with Hyperlane.
# Install dependencies
yarn
-# Build source and generate types
+# Build Next project
yarn build
```
@@ -22,11 +22,25 @@ yarn dev
## Test
```sh
-# Run all unit tests
-yarn test
-
# Lint check code
yarn lint
+
+# Check code types
+yarn typecheck
+```
+
+## Format
+
+```sh
+# Format code using Prettier
+yarn prettier
+```
+
+## Clean / Reset
+
+```sh
+# Delete build artifacts to start fresh
+yarn clean
```
## Learn more
diff --git a/next.config.js b/next.config.js
index 63933fb..3c9da16 100755
--- a/next.config.js
+++ b/next.config.js
@@ -21,6 +21,7 @@ const securityHeaders = [
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
+ // Note, causes a problem for firefox: https://github.com/MetaMask/metamask-extension/issues/3133
{
key: 'Content-Security-Policy',
value: `default-src 'self'; script-src 'self'${
diff --git a/public/background-texture2.png b/public/background-texture2.png
deleted file mode 100644
index 4fe9aa0..0000000
Binary files a/public/background-texture2.png and /dev/null differ
diff --git a/public/background-texture3.png b/public/background-texture3.png
deleted file mode 100644
index 8ec8e34..0000000
Binary files a/public/background-texture3.png and /dev/null differ
diff --git a/src/components/animation/Fade.tsx b/src/components/animation/Fade.tsx
deleted file mode 100644
index 466355b..0000000
--- a/src/components/animation/Fade.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { PropsWithChildren, useEffect, useState } from 'react';
-
-export function Fade(props: PropsWithChildren<{ show: boolean }>) {
- const { show, children } = props;
- const [render, setRender] = useState(show);
-
- useEffect(() => {
- if (show) setRender(true);
- }, [show]);
-
- const onAnimationEnd = () => {
- if (!show) setRender(false);
- };
-
- return render ? (
-
- {children}
-
- ) : null;
-}
diff --git a/src/components/buttons/BackButton.tsx b/src/components/buttons/BackButton.tsx
deleted file mode 100644
index c1c4694..0000000
--- a/src/components/buttons/BackButton.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import LeftArrow from '../../images/icons/arrow-left-circle.svg';
-
-import { IconButton, IconButtonProps } from './IconButton';
-
-export function BackButton(props: IconButtonProps) {
- return ;
-}
diff --git a/src/components/buttons/BorderedButton.tsx b/src/components/buttons/BorderedButton.tsx
deleted file mode 100644
index 3a5633d..0000000
--- a/src/components/buttons/BorderedButton.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { PropsWithChildren, ReactElement } from 'react';
-
-interface ButtonProps {
- type?: 'submit' | 'reset' | 'button';
- onClick?: () => void;
- classes?: string;
- bold?: boolean;
- disabled?: boolean;
- icon?: ReactElement;
- title?: string;
-}
-
-export function BorderedButton(props: PropsWithChildren) {
- const { type, onClick, classes, bold, icon, disabled, title } = props;
-
- const base = 'border border-black rounded transition-all';
- const onHover = 'hover:border-gray-500 hover:text-gray-500';
- const onDisabled = 'disabled:border-gray-300 disabled:text-gray-300';
- const onActive = 'active:border-gray-400 active:text-gray-400';
- const weight = bold ? 'font-semibold' : '';
- const allClasses = `${base} ${onHover} ${onDisabled} ${onActive} ${weight} ${classes}`;
-
- return (
-
- );
-}
diff --git a/src/components/buttons/TextButton.tsx b/src/components/buttons/TextButton.tsx
deleted file mode 100644
index 56409dd..0000000
--- a/src/components/buttons/TextButton.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { PropsWithChildren } from 'react';
-
-export interface TextButtonProps {
- classes?: string;
- onClick?: () => void;
- disabled?: boolean;
- type?: 'button' | 'submit';
- passThruProps?: any;
-}
-
-export function TextButton(props: PropsWithChildren) {
- const { classes, onClick, disabled, type, children, passThruProps } = props;
-
- const base = 'flex place-content-center transition-all';
- const onHover = 'hover:opacity-70';
- const onDisabled = 'disabled:opacity-50';
- const onActive = 'active:opacity-60';
- const allClasses = `${base} ${onHover} ${onDisabled} ${onActive} ${classes}`;
-
- return (
-
- );
-}
diff --git a/src/components/layout/HrDivider.tsx b/src/components/layout/HrDivider.tsx
deleted file mode 100644
index 3808253..0000000
--- a/src/components/layout/HrDivider.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-interface Props {
- classes?: string;
-}
-
-export function HrDivider(props: Props) {
- const { classes } = props;
- return
;
-}
diff --git a/src/features/tokens/useTokenBalance.tsx b/src/features/tokens/useTokenBalance.tsx
index c05ec3c..ab759bb 100644
--- a/src/features/tokens/useTokenBalance.tsx
+++ b/src/features/tokens/useTokenBalance.tsx
@@ -1,10 +1,19 @@
-import { useQuery } from '@tanstack/react-query';
+import { QueryClient, useQuery } from '@tanstack/react-query';
import { useAccount } from 'wagmi';
import { logger } from '../../utils/logger';
import { getErc20Contract } from '../contracts/erc20';
import { getProvider } from '../providers';
+export function getTokenBalanceKey(
+ chainId: number,
+ tokenAddress: Address,
+ isConnected: boolean,
+ accountAddress?: Address,
+) {
+ return ['tokenBalance', chainId, tokenAddress, accountAddress, isConnected];
+}
+
export function useTokenBalance(chainId: number, tokenAddress: Address) {
const { address: accountAddress, isConnected } = useAccount();
@@ -13,7 +22,7 @@ export function useTokenBalance(chainId: number, tokenAddress: Address) {
isError: hasError,
data: balance,
} = useQuery(
- ['tokenBalance', chainId, tokenAddress, accountAddress, isConnected],
+ getTokenBalanceKey(chainId, tokenAddress, isConnected, accountAddress),
() => {
if (!chainId || !tokenAddress || !accountAddress || !isConnected) return null;
return fetchTokenBalance(chainId, tokenAddress, accountAddress);
@@ -24,6 +33,18 @@ export function useTokenBalance(chainId: number, tokenAddress: Address) {
return { isFetching, hasError, balance };
}
+export function getCachedTokenBalance(
+ queryClient: QueryClient,
+ chainId: number,
+ tokenAddress: Address,
+ isConnected: boolean,
+ accountAddress?: Address,
+) {
+ return queryClient.getQueryData(
+ getTokenBalanceKey(chainId, tokenAddress, isConnected, accountAddress),
+ ) as string | undefined;
+}
+
async function fetchTokenBalance(chainId: number, tokenAddress: Address, accountAddress: Address) {
logger.debug(
`Fetching balance for account ${accountAddress} token ${tokenAddress} on chain ${chainId}`,
diff --git a/src/features/transfer/TransferTokenForm.tsx b/src/features/transfer/TransferTokenForm.tsx
index b35247a..f89cab7 100644
--- a/src/features/transfer/TransferTokenForm.tsx
+++ b/src/features/transfer/TransferTokenForm.tsx
@@ -1,5 +1,7 @@
+import { useQueryClient } from '@tanstack/react-query';
import { Form, Formik, useFormikContext } from 'formik';
import { useState } from 'react';
+import { useAccount } from 'wagmi';
import { chainIdToMetadata, chainMetadata } from '@hyperlane-xyz/sdk';
@@ -10,6 +12,7 @@ import { ChevronIcon } from '../../components/icons/Chevron';
import { HyperlaneChevron, HyperlaneWideChevron } from '../../components/icons/HyperlaneChevron';
import { TextField } from '../../components/input/TextField';
import { Card } from '../../components/layout/Card';
+import { config } from '../../consts/config';
import GearIcon from '../../images/icons/gear.svg';
import SwapIcon from '../../images/icons/swap.svg';
import { Color } from '../../styles/Color';
@@ -19,7 +22,7 @@ import { getChainDisplayName, getChainEnvironment } from '../../utils/chains';
import { logger } from '../../utils/logger';
import { ChainSelectField } from '../chains/ChainSelectField';
import { TokenSelectField } from '../tokens/TokenSelectField';
-import { useTokenBalance } from '../tokens/useTokenBalance';
+import { getCachedTokenBalance, useTokenBalance } from '../tokens/useTokenBalance';
import { TransferTransactionsModal } from './TransferTransactionsModal';
import { TransferFormValues } from './types';
@@ -47,6 +50,8 @@ export function TransferTokenForm() {
setIsReview(false);
};
+ const queryClient = useQueryClient();
+ const { address: accountAddress, isConnected } = useAccount();
const validateForm = ({
sourceChainId,
destinationChainId,
@@ -64,11 +69,6 @@ export function TransferTokenForm() {
if (getChainEnvironment(sourceChainId) !== getChainEnvironment(destinationChainId)) {
return { destinationChainId: 'Invalid chain combination' };
}
- // TODO check balance and check non-zero
- const parsedAmount = tryParseAmount(amount);
- if (!parsedAmount || parsedAmount.lte(0)) {
- return { amount: 'Invalid amount' };
- }
if (!isValidAddress(recipientAddress)) {
return { recipientAddress: 'Invalid recipient' };
}
@@ -78,6 +78,20 @@ export function TransferTokenForm() {
if (!isValidAddress(hypCollateralAddress)) {
return { tokenAddress: 'Invalid collateral token' };
}
+ const parsedAmount = tryParseAmount(amount);
+ if (!parsedAmount || parsedAmount.lte(0)) {
+ return { amount: 'Invalid amount' };
+ }
+ const cachedBalance = getCachedTokenBalance(
+ queryClient,
+ sourceChainId,
+ tokenAddress,
+ isConnected,
+ accountAddress,
+ );
+ if (cachedBalance && parsedAmount.gt(cachedBalance) && !config.debug) {
+ return { amount: 'Insufficient balance' };
+ }
return {};
};
@@ -110,59 +124,56 @@ export function TransferTokenForm() {
validateOnBlur={false}
>
{({ values }) => (
- <>
-