Merge branch 'main' into gas-payment

pull/14/head
J M Rossy 2 years ago
commit d618e90001
  1. 33
      package.json
  2. 97
      src/components/icons/ChainIcon.tsx
  3. 84
      src/components/icons/HyperlaneChevron.tsx
  4. 11
      src/features/chains/ChainSelectField.tsx
  5. 9
      src/features/chains/ChainSelectModal.tsx
  6. 13
      src/features/chains/metadata.ts
  7. 24
      src/features/tokens/routes.ts
  8. 5
      src/features/transfer/TransferTokenCard.tsx
  9. 41
      src/features/transfer/TransferTokenForm.tsx
  10. 101
      src/features/transfer/TransferTransactionsModal.tsx
  11. 22
      src/features/transfer/useTokenTransfer.ts
  12. 5
      src/features/wallet/WalletControlBar.tsx
  13. 2
      src/pages/_app.tsx
  14. 885
      yarn.lock

@ -4,38 +4,39 @@
"version": "1.0.0",
"author": "J M Rossy",
"dependencies": {
"@headlessui/react": "^1.7.7",
"@hyperlane-xyz/hyperlane-token": "^1.1.1",
"@hyperlane-xyz/sdk": "^1.1.1",
"@headlessui/react": "^1.7.8",
"@hyperlane-xyz/hyperlane-token": "^1.2.0",
"@hyperlane-xyz/sdk": "^1.2.0",
"@hyperlane-xyz/widgets": "^1.2.0",
"@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6",
"@rainbow-me/rainbowkit": "^0.8.1",
"@tanstack/react-query": "^4.20.4",
"@tanstack/react-query": "^4.24.4",
"bignumber.js": "^9.0.2",
"buffer": "^6.0.3",
"ethers": "^5.7.2",
"formik": "^2.2.9",
"next": "^13.1.0",
"next": "^13.1.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-toastify": "^9.1.1",
"wagmi": "^0.10.4"
"wagmi": "0.10.4"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.0.0",
"@types/node": "^18.11.17",
"@types/react": "^18.0.16",
"@types/node": "^18.11.18",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.47.0",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"autoprefixer": "^10.4.13",
"eslint": "^8.30.0",
"eslint-config-next": "^13.1.0",
"eslint-config-prettier": "^8.5.0",
"postcss": "^8.4.20",
"prettier": "^2.8.1",
"eslint": "^8.33.0",
"eslint-config-next": "^13.1.6",
"eslint-config-prettier": "^8.6.0",
"postcss": "^8.4.21",
"prettier": "^2.8.3",
"tailwindcss": "^3.2.4",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
"typescript": "^4.9.5"
},
"homepage": "https://www.hyperlane.xyz",
"license": "Apache-2.0",

@ -1,97 +0,0 @@
import Image from 'next/image';
import { memo } from 'react';
import { chainMetadata } from '@hyperlane-xyz/sdk';
import ArbitrumMono from '@hyperlane-xyz/sdk/logos/black/arbitrum.svg';
import AvalancheMono from '@hyperlane-xyz/sdk/logos/black/avalanche.svg';
import BscMono from '@hyperlane-xyz/sdk/logos/black/bsc.svg';
import CeloMono from '@hyperlane-xyz/sdk/logos/black/celo.svg';
import EthereumMono from '@hyperlane-xyz/sdk/logos/black/ethereum.svg';
import MoonbeamMono from '@hyperlane-xyz/sdk/logos/black/moonbeam.svg';
import OptimismMono from '@hyperlane-xyz/sdk/logos/black/optimism.svg';
import PolygonMono from '@hyperlane-xyz/sdk/logos/black/polygon.svg';
import ArbitrumColor from '@hyperlane-xyz/sdk/logos/color/arbitrum.svg';
import AvalancheColor from '@hyperlane-xyz/sdk/logos/color/avalanche.svg';
import BscColor from '@hyperlane-xyz/sdk/logos/color/bsc.svg';
import CeloColor from '@hyperlane-xyz/sdk/logos/color/celo.svg';
import EthereumColor from '@hyperlane-xyz/sdk/logos/color/ethereum.svg';
import MoonbeamColor from '@hyperlane-xyz/sdk/logos/color/moonbeam.svg';
import OptimismColor from '@hyperlane-xyz/sdk/logos/color/optimism.svg';
import PolygonColor from '@hyperlane-xyz/sdk/logos/color/polygon.svg';
import { chainIdToCustomConfig, getChainDisplayName } from '../../features/chains/metadata';
import QuestionMark from '../../images/icons/question-mark.svg';
// Keep up to date as new chains are added or
// icon will fallback to default
const CHAIN_TO_MONOCHROME_ICON = {
[chainMetadata.alfajores.id]: CeloMono,
[chainMetadata.arbitrum.id]: ArbitrumMono,
[chainMetadata.arbitrumgoerli.id]: ArbitrumMono,
[chainMetadata.avalanche.id]: AvalancheMono,
[chainMetadata.bsc.id]: BscMono,
[chainMetadata.bsctestnet.id]: BscMono,
[chainMetadata.celo.id]: CeloMono,
[chainMetadata.ethereum.id]: EthereumMono,
[chainMetadata.fuji.id]: AvalancheMono,
[chainMetadata.goerli.id]: EthereumMono,
[chainMetadata.moonbasealpha.id]: MoonbeamMono,
[chainMetadata.moonbeam.id]: MoonbeamMono,
[chainMetadata.mumbai.id]: PolygonMono,
[chainMetadata.optimism.id]: OptimismMono,
[chainMetadata.optimismgoerli.id]: OptimismMono,
[chainMetadata.polygon.id]: PolygonMono,
};
const CHAIN_TO_COLOR_ICON = {
[chainMetadata.alfajores.id]: CeloColor,
[chainMetadata.arbitrum.id]: ArbitrumColor,
[chainMetadata.arbitrumgoerli.id]: ArbitrumColor,
[chainMetadata.avalanche.id]: AvalancheColor,
[chainMetadata.bsc.id]: BscColor,
[chainMetadata.bsctestnet.id]: BscColor,
[chainMetadata.celo.id]: CeloColor,
[chainMetadata.ethereum.id]: EthereumColor,
[chainMetadata.fuji.id]: AvalancheColor,
[chainMetadata.goerli.id]: EthereumColor,
[chainMetadata.moonbasealpha.id]: MoonbeamColor,
[chainMetadata.moonbeam.id]: MoonbeamColor,
[chainMetadata.mumbai.id]: PolygonColor,
[chainMetadata.optimism.id]: OptimismColor,
[chainMetadata.optimismgoerli.id]: OptimismColor,
[chainMetadata.polygon.id]: PolygonColor,
};
interface Props {
chainId?: number;
size?: number;
color?: boolean;
background?: boolean;
}
function _ChainIcon({ chainId, size = 32, color = true, background = false }: Props) {
const iconSet = color ? CHAIN_TO_COLOR_ICON : CHAIN_TO_MONOCHROME_ICON;
const imageSrc =
(chainId && (iconSet[chainId] || chainIdToCustomConfig[chainId]?.logoImgSrc)) || QuestionMark;
if (background) {
return (
<div
style={{ width: `${size}px`, height: `${size}px` }}
className="flex items-center justify-center rounded-full bg-gray-100 transition-all"
title={getChainDisplayName(chainId)}
>
<Image
src={imageSrc}
alt=""
width={Math.floor(size / 1.8)}
height={Math.floor(size / 1.8)}
/>
</div>
);
} else {
return <Image src={imageSrc} alt="" width={size} height={size} />;
}
}
export const ChainIcon = memo(_ChainIcon);

@ -1,84 +0,0 @@
import { memo } from 'react';
import { Color } from '../../styles/Color';
interface Props {
width?: string | number;
height?: string | number;
direction: 'n' | 'e' | 's' | 'w';
color?: string;
classes?: string;
}
function _HyperlaneChevron({ width, height, direction, color, classes }: Props) {
let directionClass;
switch (direction) {
case 'n':
directionClass = '-rotate-90';
break;
case 'e':
directionClass = '';
break;
case 's':
directionClass = 'rotate-90';
break;
case 'w':
directionClass = 'rotate-180';
break;
default:
throw new Error(`Invalid chevron direction ${direction}`);
}
return (
<svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="1.2 1 139.1 322"
width={width}
height={height}
className={`${directionClass} ${classes}`}
>
<path
d="M6.3 1h61.3a20 20 0 0 1 18.7 13L140 158.3a5 5 0 0 1 0 3.4l-.3.9-53.5 147.2A20 20 0 0 1 67.4 323H6.2a5 5 0 0 1-4.7-6.6l55.2-158.1L1.7 7.7A5 5 0 0 1 6.2 1Z"
fill={color || Color.primaryBlue}
></path>
</svg>
);
}
export const HyperlaneChevron = memo(_HyperlaneChevron);
function _HyperlaneWideChevron({ width, height, direction, color, classes }: Props) {
let directionClass;
switch (direction) {
case 'n':
directionClass = '-rotate-90';
break;
case 'e':
directionClass = '';
break;
case 's':
directionClass = 'rotate-90';
break;
case 'w':
directionClass = 'rotate-180';
break;
default:
throw new Error(`Invalid chevron direction ${direction}`);
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 120.3 190"
width={width}
height={height}
className={`${directionClass} ${classes}`}
fill={color || Color.primaryBlue}
>
<path d="M4.4 0h53c7.2 0 13.7 3 16.2 7.7l46.5 85.1a2 2 0 0 1 0 2l-.2.5-46.3 87c-2.5 4.6-9 7.7-16.3 7.7h-53c-3 0-5-2-4-4L48 92.9.4 4c-1-2 1-4 4-4Z" />
</svg>
);
}
export const HyperlaneWideChevron = memo(_HyperlaneWideChevron);

@ -2,7 +2,8 @@ import { useField } from 'formik';
import Image from 'next/image';
import { useState } from 'react';
import { ChainIcon } from '../../components/icons/ChainIcon';
import { ChainLogo } from '@hyperlane-xyz/widgets';
import ChevronIcon from '../../images/icons/chevron-down.svg';
import { ChainSelectListModal } from './ChainSelectModal';
@ -32,11 +33,11 @@ export function ChainSelectField({ name, label, chainIds, onChange, disabled }:
return (
<div className="flex flex-col items-center">
<div className="flex flex-col items-center justify-center rounded-full bg-gray-100 h-[6rem] w-[6rem] p-1.5">
<div className="flex flex-col items-center justify-center rounded-full bg-gray-100 h-[5.5rem] w-[5.5rem] p-1.5">
<div className="flex items-end h-11">
<ChainIcon chainId={field.value} size={32} />
<ChainLogo chainId={field.value} size={34} />
</div>
<label htmlFor={name} className="mt-2.5 text-sm text-gray-500 uppercase">
<label htmlFor={name} className="mt-2 mb-1 text-sm text-gray-500 uppercase">
{label}
</label>
</div>
@ -47,7 +48,7 @@ export function ChainSelectField({ name, label, chainIds, onChange, disabled }:
onClick={onClick}
>
<div className="flex items-center">
<ChainIcon chainId={field.value} size={14} />
<ChainLogo chainId={field.value} size={14} />
<span className="ml-2">{getChainDisplayName(field.value, true)}</span>
</div>
<Image src={ChevronIcon} width={12} height={8} alt="" />

@ -1,4 +1,5 @@
import { ChainIcon } from '../../components/icons/ChainIcon';
import { ChainLogo } from '@hyperlane-xyz/widgets';
import { Modal } from '../../components/layout/Modal';
import { getChainDisplayName, getChainMetadata } from './metadata';
@ -32,7 +33,7 @@ export function ChainSelectListModal({
className="py-1.5 px-2 text-sm flex items-center rounded hover:bg-gray-100 active:bg-gray-200 transition-all duration-200"
onClick={onSelectChain(c.id)}
>
<ChainIcon chainId={c.id} size={16} background={false} />
<ChainLogo chainId={c.id} size={16} background={false} />
<span className="ml-2">{getChainDisplayName(c.id, true)}</span>
</button>
))}
@ -69,7 +70,7 @@ export function ChainSelectListModal({
// className="py-1.5 px-2 text-sm flex items-center rounded hover:bg-gray-100 active:bg-gray-200 transition-all duration-200"
// onClick={onSelectChain(c.id)}
// >
// <ChainIcon chainId={c.id} size={16} background={false} />
// <ChainLogo chainId={c.id} size={16} background={false} />
// <span className="ml-2">{getChainDisplayName(c.id, true)}</span>
// </button>
// ))}
@ -82,7 +83,7 @@ export function ChainSelectListModal({
// className="py-1.5 px-2 text-sm flex items-center rounded hover:bg-gray-100 active:bg-gray-200 transition-all duration-200"
// onClick={onSelectChain(c.id)}
// >
// <ChainIcon chainId={c.id} size={16} background={false} />
// <ChainLogo chainId={c.id} size={16} background={false} />
// <span className="ml-2">{getChainDisplayName(c.id, true)}</span>
// </button>
// ))}

@ -1,6 +1,6 @@
import type { Chain as WagmiChain } from '@wagmi/chains';
import type { Chain as WagmiChain } from '@wagmi/core';
import { ChainMetadata, chainIdToMetadata, objMap, wagmiChainMetadata } from '@hyperlane-xyz/sdk';
import { ChainMetadata, chainIdToMetadata, objMap } from '@hyperlane-xyz/sdk';
import CustomChainConfig from '../../consts/chains.json';
@ -41,9 +41,9 @@ export function getChainDisplayName(chainId?: number, shortName = false): string
}
// Metadata formatted for use in Wagmi config
export function getWagmiChainConfig() {
export function getWagmiChainConfig(): WagmiChain[] {
return Object.values({
...wagmiChainMetadata,
...objMap(chainIdToMetadata, toWagmiConfig),
...objMap(chainIdToCustomConfig as Record<string, ChainMetadata>, toWagmiConfig),
});
}
@ -55,7 +55,10 @@ function toWagmiConfig(_: any, metadata: ChainMetadata): WagmiChain {
name: metadata.displayName,
network: metadata.name as string,
nativeCurrency: metadata.nativeToken,
rpcUrls: { default: { http: [metadata.publicRpcUrls[0].http] } },
rpcUrls: {
default: { http: [metadata.publicRpcUrls[0].http] },
public: { http: [metadata.publicRpcUrls[0].http] },
},
blockExplorers: metadata.blockExplorers.length
? {
default: {

@ -4,6 +4,7 @@ import { useMemo } from 'react';
import { utils } from '@hyperlane-xyz/utils';
import { areAddressesEqual, isValidAddress, normalizeAddress } from '../../utils/addresses';
import { logger } from '../../utils/logger';
import { getHypErc20CollateralContract } from '../contracts/hypErc20';
import { getProvider } from '../providers';
@ -138,23 +139,28 @@ export function useTokenRoutes() {
} = useQuery(
['token-routes'],
async () => {
logger.info('Searching for token routes');
const tokens: ListedTokenWithHypTokens[] = [];
for (const token of getAllTokens()) {
logger.info('Inspecting token:', token.symbol);
const provider = getProvider(token.chainId);
const collateralContract = getHypErc20CollateralContract(
token.hypCollateralAddress,
provider,
);
logger.info('Fetching connected domains');
const domains = await collateralContract.domains();
const hypTokens: Array<{ chainId: number; address: Address }> = [];
// TODO parallelization here would be good, either with RPC batching or just promise.all, but
// avoiding it for now due to limitations of public RPC providers
for (const chainId of domains) {
const hypTokenAddrBytes = await collateralContract.routers(chainId);
const hypTokenAddr = utils.bytes32ToAddress(hypTokenAddrBytes);
hypTokens.push({ chainId, address: normalizeAddress(hypTokenAddr) });
}
logger.info(`Found ${domains.length} connected domains:`, domains);
logger.info('Getting domain router address');
const hypTokenByteAddressesP = domains.map((d) => collateralContract.routers(d));
const hypTokenByteAddresses = await Promise.all(hypTokenByteAddressesP);
const hypTokenAddresses = hypTokenByteAddresses.map((b) => utils.bytes32ToAddress(b));
logger.info(`Addresses found:`, hypTokenAddresses);
const hypTokens = hypTokenAddresses.map((addr, i) => ({
chainId: domains[i],
address: normalizeAddress(addr),
}));
tokens.push({ ...token, hypTokens });
}

@ -1,6 +1,7 @@
// import { IconButton } from '../../components/buttons/IconButton';
import { WideChevron } from '@hyperlane-xyz/widgets';
import { Spinner } from '../../components/animation/Spinner';
import { HyperlaneWideChevron } from '../../components/icons/HyperlaneChevron';
import { Card } from '../../components/layout/Card';
import { useTokenRoutes } from '../tokens/routes';
@ -13,7 +14,7 @@ export function TransferTokenCard() {
return (
<Card classes="w-100 sm:w-[31rem] relative">
<div className="absolute left-0 right-0 -top-32 xs:-top-24 flex justify-center overflow-hidden z-10">
<HyperlaneWideChevron direction="s" height="100%" width="100" />
<WideChevron direction="s" height="100%" width="100" rounded={true} />
</div>
<div className="relative flex items-start justify-between z-20">
<h2 className="pl-0.5 text-lg">Send Tokens</h2>

@ -3,11 +3,12 @@ import { Form, Formik, useFormikContext } from 'formik';
import { useMemo, useState } from 'react';
import { useAccount } from 'wagmi';
import { WideChevron } from '@hyperlane-xyz/widgets';
import { ConnectAwareSubmitButton } from '../../components/buttons/ConnectAwareSubmitButton';
import { IconButton } from '../../components/buttons/IconButton';
import { SolidButton } from '../../components/buttons/SolidButton';
import { ChevronIcon } from '../../components/icons/Chevron';
import { HyperlaneChevron } from '../../components/icons/HyperlaneChevron';
import { TextField } from '../../components/input/TextField';
import { config } from '../../consts/config';
import SwapIcon from '../../images/icons/swap.svg';
@ -40,6 +41,8 @@ export function TransferTokenForm({ tokenRoutes }: { tokenRoutes: RoutesMap }) {
// Flag for if form is in input vs review mode
const [isReview, setIsReview] = useState(false);
// Flag for if loading modal is open (visible)
const [isModalOpen, setIsModalOpen] = useState(false);
const onSubmitForm = (values: TransferFormValues) => {
logger.debug('Reviewing transfer form values:', JSON.stringify(values));
@ -77,15 +80,17 @@ export function TransferTokenForm({ tokenRoutes }: { tokenRoutes: RoutesMap }) {
return {};
};
const onStartTransactions = () => {
setIsModalOpen(true);
};
const onDoneTransactions = () => {
setIsReview(false);
// Consider clearing form inputs here
};
const {
isLoading: isTransferLoading,
dismissIsLoading,
triggerTransactions,
} = useTokenTransfer(onDoneTransactions);
const { triggerTransactions, originTxHash } = useTokenTransfer(
onStartTransactions,
onDoneTransactions,
);
return (
<Formik<TransferFormValues>
@ -106,15 +111,28 @@ export function TransferTokenForm({ tokenRoutes }: { tokenRoutes: RoutesMap }) {
/>
<div className="flex flex-col items-center">
<div className="flex mb-6 sm:space-x-1.5">
<HyperlaneChevron
<WideChevron
width="17"
height="100%"
direction="e"
color={Color.lightGray}
classes="hidden sm:block"
rounded={true}
/>
<WideChevron
width="17"
height="100%"
direction="e"
color={Color.lightGray}
rounded={true}
/>
<WideChevron
width="17"
height="100%"
direction="e"
color={Color.lightGray}
rounded={true}
/>
<HyperlaneChevron width="17" height="100%" direction="e" color={Color.lightGray} />
<HyperlaneChevron width="17" height="100%" direction="e" color={Color.lightGray} />
</div>
<SwapChainsButton disabled={isReview} />
</div>
@ -206,9 +224,10 @@ export function TransferTokenForm({ tokenRoutes }: { tokenRoutes: RoutesMap }) {
</div>
)}
<TransferTransactionsModal
isOpen={isTransferLoading}
close={dismissIsLoading}
isOpen={isModalOpen}
close={() => setIsModalOpen(false)}
tokenRoutes={tokenRoutes}
originTxHash={originTxHash}
/>
</Form>
)}

@ -1,8 +1,13 @@
import { useFormikContext } from 'formik';
import { useAccount } from 'wagmi';
import { chainIdToMetadata } from '@hyperlane-xyz/sdk';
import { MessageStatus, MessageTimeline, useMessageTimeline } from '@hyperlane-xyz/widgets';
import { Spinner } from '../../components/animation/Spinner';
import { Modal } from '../../components/layout/Modal';
import { links } from '../../consts/links';
import { trimLeading0x } from '../../utils/addresses';
import { RouteType, RoutesMap, getTokenRoute } from '../tokens/routes';
import { TransferFormValues } from './types';
@ -11,13 +16,16 @@ export function TransferTransactionsModal({
isOpen,
close,
tokenRoutes,
originTxHash,
}: {
isOpen: boolean;
close: () => void;
tokenRoutes: RoutesMap;
originTxHash: string | null;
}) {
const { address, isConnected, connector } = useAccount();
const isAccountReady = !!(address && isConnected && connector);
const connectorName = connector?.name || 'wallet';
const {
values: { sourceChainId, destinationChainId, tokenAddress },
@ -25,8 +33,96 @@ export function TransferTransactionsModal({
const route = getTokenRoute(sourceChainId, destinationChainId, tokenAddress, tokenRoutes);
const requiresApprove = route?.type === RouteType.NativeToRemote;
const isPermisionlessRoute = !(
chainIdToMetadata[sourceChainId] && chainIdToMetadata[destinationChainId]
);
return (
<Modal
isOpen={isOpen}
title="Token Transfer"
close={close}
width={isPermisionlessRoute ? 'max-w-xs' : 'max-w-lg'}
>
{isPermisionlessRoute ? (
<BasicSpinner
isAccountReady={isAccountReady}
requiresApprove={requiresApprove}
connectorName={connectorName}
/>
) : (
<Timeline
isAccountReady={isAccountReady}
requiresApprove={requiresApprove}
connectorName={connectorName}
originTxHash={originTxHash}
/>
)}
</Modal>
);
}
function Timeline({
isAccountReady,
requiresApprove,
connectorName,
originTxHash,
}: {
isAccountReady: boolean;
requiresApprove: boolean;
connectorName: string;
originTxHash: string | null;
}) {
const { stage, timings, message } = useMessageTimeline({
originTxHash: originTxHash || undefined,
});
return (
<div className="mt-4 mb-2 w-full flex flex-col justify-center items-center">
<MessageTimeline
status={message?.status || MessageStatus.Pending}
stage={stage}
timings={timings}
timestampSent={message?.originTransaction.timestamp}
hideDescriptions={true}
/>
{isAccountReady ? (
<>
<div className="mt-5 text-sm text-center text-gray-500">
{requiresApprove
? 'Attempting to send two transactions: Approve and TransferRemote'
: 'Attempting to send transaction: TransferRemote'}
</div>
<div className="mt-3 text-sm text-center text-gray-500">{`Sign with ${connectorName} to proceed`}</div>
</>
) : (
<div className="mt-5 text-sm text-center text-gray-500">
Please connect wallet to proceed
</div>
)}
{message && (
<a
className="mt-4 text-gray-500 underline underline-offset-2"
href={`${links.explorer}/message/${trimLeading0x(message.id)}`}
target="_blank"
rel="noopener noreferrer"
>
Open Transaction in Hyperlane Explorer
</a>
)}
</div>
);
}
function BasicSpinner({
isAccountReady,
requiresApprove,
connectorName,
}: {
isAccountReady: boolean;
requiresApprove: boolean;
connectorName: string;
}) {
return (
<Modal isOpen={isOpen} title="Token Transfer" close={close}>
<div className="my-6 flex flex-col justify-center items-center">
<Spinner />
{isAccountReady ? (
@ -36,7 +132,7 @@ export function TransferTransactionsModal({
? 'Attempting to send two transactions: Approve and TransferRemote'
: 'Attempting to send transaction: TransferRemote'}
</div>
<div className="mt-3 text-sm text-center text-gray-500">{`Sign with ${connector.name} to proceed`}</div>
<div className="mt-3 text-sm text-center text-gray-500">{`Sign with ${connectorName} to proceed`}</div>
</>
) : (
<div className="mt-5 text-sm text-center text-gray-500">
@ -44,6 +140,5 @@ export function TransferTransactionsModal({
</div>
)}
</div>
</Modal>
);
}

@ -23,15 +23,18 @@ enum Stage {
// Note, this doesn't use wagmi's prepare + send pattern because we're potentially sending two transactions
// See https://github.com/wagmi-dev/wagmi/discussions/1564
export function useTokenTransfer(onDone?: () => void) {
export function useTokenTransfer(onStart?: () => void, onDone?: () => void) {
const [isLoading, setIsLoading] = useState(false);
const dismissIsLoading = () => setIsLoading(false);
const [originTxHash, setOriginTxHash] = useState<string | null>(null);
// TODO implement cancel callback for when modal is closed?
const triggerTransactions = useCallback(
async (values: TransferFormValues, tokenRoutes: RoutesMap) => {
logger.debug('Attempting approve and transfer transactions');
setOriginTxHash(null);
setIsLoading(true);
if (onStart) onStart();
let stage: Stage = Stage.Prepare;
try {
@ -99,13 +102,10 @@ export function useTokenTransfer(onDone?: () => void) {
request: transferTxRequest,
mode: 'recklesslyUnprepared', // See note above function
});
const transferTxReceipt = await transferWait(1);
logger.debug('Transfer transaction confirmed, hash:', transferTxReceipt.transactionHash);
toastTxSuccess(
'Remote transfer started!',
transferTxReceipt.transactionHash,
sourceChainId,
);
const { transactionHash } = await transferWait(1);
setOriginTxHash(transactionHash);
logger.debug('Transfer transaction confirmed, hash:', transactionHash);
toastTxSuccess('Remote transfer started!', transactionHash, sourceChainId);
} catch (error) {
logger.error(`Error at stage ${stage} `, error);
if (JSON.stringify(error).includes('ChainMismatchError')) {
@ -119,13 +119,13 @@ export function useTokenTransfer(onDone?: () => void) {
setIsLoading(false);
if (onDone) onDone();
},
[setIsLoading, onDone],
[setIsLoading, onStart, onDone],
);
return {
isLoading,
dismissIsLoading,
triggerTransactions,
originTxHash,
};
}

@ -5,8 +5,9 @@ import { Fragment } from 'react';
import { toast } from 'react-toastify';
import { useAccount, useDisconnect, useNetwork } from 'wagmi';
import { ChainLogo } from '@hyperlane-xyz/widgets';
import { SolidButton } from '../../components/buttons/SolidButton';
import { ChainIcon } from '../../components/icons/ChainIcon';
import { Identicon } from '../../components/icons/Identicon';
import ChevronDown from '../../images/icons/chevron-down.svg';
import CopyStack from '../../images/icons/copy-stack.svg';
@ -90,7 +91,7 @@ function AccountDropdown() {
<div className="px-5 pb-3 mb-2 border-b border-gray-200">
<label className="text-sm text-gray-500">Connected to:</label>
<div className="mt-1.5 flex items-center">
<ChainIcon chainId={chain.id} size={15} />
<ChainLogo chainId={chain.id} size={15} />
<div className="ml-2 text-sm">{chain.name}</div>
</div>
</div>

@ -18,6 +18,8 @@ import 'react-toastify/dist/ReactToastify.css';
import { WagmiConfig, configureChains, createClient as createWagmiClient } from 'wagmi';
import { publicProvider } from 'wagmi/providers/public';
import '@hyperlane-xyz/widgets/styles.css';
import { ErrorBoundary } from '../components/errors/ErrorBoundary';
import { AppLayout } from '../components/layout/AppLayout';
import { getWagmiChainConfig } from '../features/chains/metadata';

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save