- {/* TODO Timeline does not support PI messages yet */}
- {isPermissionlessRoute ? (
-
- )}
- {status !== TransferStatus.ConfirmedTransfer && status !== TransferStatus.Delivered ? (
- <>
-
- If your wallet does not show a transaction request, please try the transfer again.
-
-
-
-
-
- Time:
-
-
- {date}
-
-
-
-
- From:
-
-
- {activeAccountAddress}
-
- {fromUrl && (
-
-
-
- )}
-
-
-
- To:
-
-
- {recipientAddress}
-
- {toUrl && (
-
-
-
- )}
-
-
-
-
-
-
-
- Token:
-
-
- {isNative ? 'Native currency' : tokenAddress}
-
- {tokenUrl && !isNative && (
-
-
-
- )}
-
-
-
- Origin Tx:
-
-
- {originTxHash}
-
- {originTxUrl && (
-
-
-
- )}
-
- {explorerLink && (
-
- )}
-
+
+
+
+
+
+ {getChainDisplayName(originCaip2Id, true)}
+
+
+
+
+
+
+
+
+
+ {getChainDisplayName(destinationCaip2Id, true)}
+
+
+
+
+ {isFinal ? (
+
+
+
+ {!isNative &&
}
+ {originTxHash && (
+
+ )}
+ {explorerLink && (
+
+ )}
+
+ ) : (
+
+
+
+ {statusDescription}
- )}
-
+ {showSignWarning && (
+
+ If your wallet does not show a transaction request, please try the transfer again.
+
+ )}
+
+ )}
);
}
-function Timeline({
+// TODO consider re-enabling timeline
+export function Timeline({
transferStatus,
originTxHash,
}: {
@@ -303,6 +232,25 @@ function Timeline({
);
}
+function TransferProperty({ name, value, url }: { name: string; value: string; url?: string }) {
+ return (
+
+
+
+
+ {url && (
+
+
+
+ )}
+
+
+
+
{value}
+
+ );
+}
+
// TODO: Remove this once we have a better solution for wagmi signing issue
// https://github.com/wagmi-dev/wagmi/discussions/2928
function useSignIssueWarning(status: TransferStatus) {
@@ -310,6 +258,12 @@ function useSignIssueWarning(status: TransferStatus) {
const warningCallback = useCallback(() => {
if (status === TransferStatus.SigningTransfer) setShowWarning(true);
}, [status, setShowWarning]);
- useTimeout(warningCallback, 15_000);
+ useTimeout(warningCallback, 20_000);
return showWarning;
}
+
+// TODO cosmos fix double slash problem in ChainMetadataManager
+// Occurs when baseUrl has not other path (e.g. for manta explorer)
+function fixDoubleSlash(url: string) {
+ return url.replace(/([^:]\/)\/+/g, '$1');
+}
diff --git a/src/features/transfer/components/TransferStatusIcon.tsx b/src/features/transfer/components/TransferStatusIcon.tsx
deleted file mode 100644
index e7a27edb..00000000
--- a/src/features/transfer/components/TransferStatusIcon.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import Image from 'next/image';
-
-import { Spinner } from '../../../components/animation/Spinner';
-import CheckmarkCircleIcon from '../../../images/icons/checkmark-circle.svg';
-import EnvelopeHeartIcon from '../../../images/icons/envelope-heart.svg';
-import ErrorCircleIcon from '../../../images/icons/error-circle.svg';
-import { TransferStatus } from '../types';
-
-export function TransferStatusIcon({ transferStatus }: { transferStatus: TransferStatus }) {
- let content;
- if (transferStatus === TransferStatus.Delivered) {
- content = (
-
- );
- } else if (transferStatus === TransferStatus.ConfirmedTransfer) {
- content = (
-
- );
- } else if (transferStatus === TransferStatus.Failed) {
- content = (
-
- );
- } else {
- content =
;
- }
- return
{content}
;
-}
diff --git a/src/features/transfer/types.ts b/src/features/transfer/types.ts
index 78a9ce5a..9b658584 100644
--- a/src/features/transfer/types.ts
+++ b/src/features/transfer/types.ts
@@ -21,12 +21,10 @@ export enum TransferStatus {
Failed = 'failed',
}
+export const SentTransferStatuses = [TransferStatus.ConfirmedTransfer, TransferStatus.Delivered];
+
// Statuses considered not pending
-export const FinalTransferStatuses = [
- TransferStatus.ConfirmedTransfer,
- TransferStatus.Delivered,
- TransferStatus.Failed,
-];
+export const FinalTransferStatuses = [...SentTransferStatuses, TransferStatus.Failed];
export interface TransferContext {
status: TransferStatus;
diff --git a/src/features/transfer/useTokenTransfer.ts b/src/features/transfer/useTokenTransfer.ts
index a0dd42e6..eecfaf8d 100644
--- a/src/features/transfer/useTokenTransfer.ts
+++ b/src/features/transfer/useTokenTransfer.ts
@@ -1,31 +1,44 @@
+import { MsgTransferEncodeObject } from '@cosmjs/stargate';
import type { Transaction as SolTransaction } from '@solana/web3.js';
import { BigNumber, PopulatedTransaction as EvmTransaction, providers } from 'ethers';
import { useCallback, useState } from 'react';
import { toast } from 'react-toastify';
-import { HyperlaneCore, IHypTokenAdapter } from '@hyperlane-xyz/sdk';
-import { ProtocolType, convertDecimals, toWei } from '@hyperlane-xyz/utils';
+import {
+ CosmIbcToWarpTokenAdapter,
+ CosmIbcTokenAdapter,
+ IHypTokenAdapter,
+} from '@hyperlane-xyz/sdk';
+import { ProtocolType, toWei } from '@hyperlane-xyz/utils';
import { toastTxSuccess } from '../../components/toast/TxSuccessToast';
+import { COSM_IGP_QUOTE } from '../../consts/values';
import { logger } from '../../utils/logger';
-import { getProtocolType, parseCaip2Id } from '../caip/chains';
+import { parseCaip2Id } from '../caip/chains';
import { isNonFungibleToken } from '../caip/tokens';
-import { getMultiProvider } from '../multiProvider';
+import { getChainMetadata, getMultiProvider } from '../multiProvider';
import { AppState, useStore } from '../store';
import { AdapterFactory } from '../tokens/AdapterFactory';
import { isApproveRequired } from '../tokens/approval';
import { Route, RoutesMap } from '../tokens/routes/types';
-import { getTokenRoute, isRouteFromNative, isRouteToCollateral } from '../tokens/routes/utils';
import {
- AccountInfo,
+ getTokenRoute,
+ isIbcOnlyRoute,
+ isIbcRoute,
+ isRouteFromNative,
+ isWarpRoute,
+} from '../tokens/routes/utils';
+import {
ActiveChainInfo,
SendTransactionFn,
+ getAccountAddressForChain,
useAccounts,
useActiveChains,
useTransactionFns,
} from '../wallet/hooks';
import { TransferContext, TransferFormValues, TransferStatus } from './types';
+import { ensureSufficientCollateral, tryGetMsgIdFromEvmTransferReceipt } from './utils';
export function useTokenTransfer(onDone?: () => void) {
const { transfers, addTransfer, updateTransferStatus } = useStore((s) => ({
@@ -112,7 +125,12 @@ async function executeTransfer({
const isNft = isNonFungibleToken(tokenCaip19Id);
const weiAmountOrId = isNft ? amount : toWei(amount, tokenRoute.originDecimals).toFixed(0);
- const activeAccountAddress = activeAccounts.accounts[originProtocol]?.address || '';
+ const activeAccountAddress = getAccountAddressForChain(
+ originCaip2Id,
+ activeAccounts.accounts[originProtocol],
+ );
+ if (!activeAccountAddress) throw new Error('No active account found for origin chain');
+ const activeChain = activeChains.chains[originProtocol];
addTransfer({
activeAccountAddress,
@@ -122,37 +140,29 @@ async function executeTransfer({
params: values,
});
- await ensureSufficientCollateral(tokenRoute, weiAmountOrId, isNft);
-
- const hypTokenAdapter = AdapterFactory.HypTokenAdapterFromRouteOrigin(tokenRoute);
-
- const triggerParams: ExecuteTransferParams
= {
+ const executeParams: ExecuteTransferParams = {
weiAmountOrId,
+ originProtocol,
destinationDomainId,
recipientAddress,
tokenRoute,
- hypTokenAdapter,
- activeAccount: activeAccounts.accounts[originProtocol],
- activeChain: activeChains.chains[originProtocol],
+ activeAccountAddress,
+ activeChain,
updateStatus: (s: TransferStatus) => {
status = s;
updateTransferStatus(transferIndex, s);
},
sendTransaction: transactionFns[originProtocol].sendTransaction,
};
+
let transferTxHash: string;
let msgId: string | undefined;
- if (originProtocol === ProtocolType.Ethereum) {
- const result = await executeEvmTransfer(triggerParams);
- ({ transferTxHash, msgId } = result);
- } else if (originProtocol === ProtocolType.Sealevel) {
- const result = await executeSealevelTransfer(triggerParams);
- ({ transferTxHash } = result);
- } else if (originProtocol === ProtocolType.Cosmos) {
- const result = await executeCosmWasmTransfer(triggerParams);
- ({ transferTxHash } = result);
+ if (isWarpRoute(tokenRoute)) {
+ ({ transferTxHash, msgId } = await executeHypTransfer(executeParams));
+ } else if (isIbcRoute(tokenRoute)) {
+ ({ transferTxHash } = await executeIbcTransfer(executeParams));
} else {
- throw new Error(`Unsupported protocol type: ${originProtocol}`);
+ throw new Error('Unsupported route type');
}
updateTransferStatus(transferIndex, (status = TransferStatus.ConfirmedTransfer), {
@@ -177,61 +187,59 @@ async function executeTransfer({
if (onDone) onDone();
}
-// In certain cases, like when a synthetic token has >1 collateral tokens
-// it's possible that the collateral contract balance is insufficient to
-// cover the remote transfer. This ensures the balance is sufficient or throws.
-async function ensureSufficientCollateral(route: Route, weiAmount: string, isNft?: boolean) {
- if (!isRouteToCollateral(route) || isNft) return;
-
- // TODO cosmos support here
- if (
- getProtocolType(route.originCaip2Id) === ProtocolType.Cosmos ||
- getProtocolType(route.destCaip2Id) === ProtocolType.Cosmos
- )
- return;
-
- logger.debug('Ensuring collateral balance for route', route);
- const adapter = AdapterFactory.HypTokenAdapterFromRouteDest(route);
- const destinationBalance = await adapter.getBalance(route.destRouterAddress);
- const destinationBalanceInOriginDecimals = convertDecimals(
- route.destDecimals,
- route.originDecimals,
- destinationBalance,
- );
- if (destinationBalanceInOriginDecimals.lt(weiAmount)) {
- toast.error('Collateral contract balance insufficient for transfer');
- throw new Error('Insufficient collateral balance');
- }
-}
-
interface ExecuteTransferParams {
weiAmountOrId: string;
+ originProtocol: ProtocolType;
destinationDomainId: DomainId;
recipientAddress: Address;
tokenRoute: Route;
- hypTokenAdapter: IHypTokenAdapter;
- activeAccount: AccountInfo;
+ activeAccountAddress: Address;
activeChain: ActiveChainInfo;
updateStatus: (s: TransferStatus) => void;
sendTransaction: SendTransactionFn;
}
+interface ExecuteHypTransferParams extends ExecuteTransferParams {
+ hypTokenAdapter: IHypTokenAdapter;
+}
+
+async function executeHypTransfer(params: ExecuteTransferParams) {
+ const { tokenRoute, weiAmountOrId, originProtocol } = params;
+ const hypTokenAdapter = AdapterFactory.HypTokenAdapterFromRouteOrigin(tokenRoute);
+ const paramsWithAdapter = { ...params, hypTokenAdapter };
+
+ await ensureSufficientCollateral(tokenRoute, weiAmountOrId);
+
+ let result: { transferTxHash: string; msgId?: string };
+ if (originProtocol === ProtocolType.Ethereum) {
+ result = await executeEvmTransfer(paramsWithAdapter);
+ } else if (originProtocol === ProtocolType.Sealevel) {
+ result = await executeSealevelTransfer(paramsWithAdapter);
+ } else if (originProtocol === ProtocolType.Cosmos) {
+ result = await executeCosmWasmTransfer(paramsWithAdapter);
+ } else {
+ throw new Error(`Unsupported protocol type: ${originProtocol}`);
+ }
+ return result;
+}
+
async function executeEvmTransfer({
weiAmountOrId,
destinationDomainId,
recipientAddress,
tokenRoute,
hypTokenAdapter,
- activeAccount,
+ activeAccountAddress,
activeChain,
updateStatus,
sendTransaction,
-}: ExecuteTransferParams) {
+}: ExecuteHypTransferParams) {
+ if (!isWarpRoute(tokenRoute)) throw new Error('Unsupported route type');
const { baseRouterAddress, originCaip2Id, baseTokenCaip19Id } = tokenRoute;
const isApproveTxRequired =
- activeAccount.address &&
- (await isApproveRequired(tokenRoute, baseTokenCaip19Id, weiAmountOrId, activeAccount.address));
+ activeAccountAddress &&
+ (await isApproveRequired(tokenRoute, baseTokenCaip19Id, weiAmountOrId, activeAccountAddress));
if (isApproveTxRequired) {
updateStatus(TransferStatus.CreatingApprove);
@@ -289,11 +297,11 @@ async function executeSealevelTransfer({
recipientAddress,
tokenRoute,
hypTokenAdapter,
- activeAccount,
+ activeAccountAddress,
activeChain,
updateStatus,
sendTransaction,
-}: ExecuteTransferParams) {
+}: ExecuteHypTransferParams) {
const { originCaip2Id } = tokenRoute;
updateStatus(TransferStatus.CreatingTransfer);
@@ -306,7 +314,7 @@ async function executeSealevelTransfer({
weiAmountOrId,
destination: destinationDomainId,
recipient: recipientAddress,
- fromAccountOwner: activeAccount.address,
+ fromAccountOwner: activeAccountAddress,
})) as SolTransaction;
updateStatus(TransferStatus.SigningTransfer);
@@ -332,20 +340,19 @@ async function executeCosmWasmTransfer({
activeChain,
updateStatus,
sendTransaction,
-}: ExecuteTransferParams) {
+}: ExecuteHypTransferParams) {
updateStatus(TransferStatus.CreatingTransfer);
const transferTxRequest = (await hypTokenAdapter.populateTransferRemoteTx({
weiAmountOrId,
recipient: recipientAddress,
destination: destinationDomainId,
- // TODO cosmos quote real interchain gas payment
- txValue: '25000',
+ txValue: COSM_IGP_QUOTE,
})) as EvmTransaction;
updateStatus(TransferStatus.SigningTransfer);
const { hash: transferTxHash, confirm: confirmTransfer } = await sendTransaction({
- tx: transferTxRequest,
+ tx: { type: 'cosmwasm', request: transferTxRequest },
chainCaip2Id: tokenRoute.originCaip2Id,
activeCap2Id: activeChain.chainCaip2Id,
});
@@ -356,21 +363,66 @@ async function executeCosmWasmTransfer({
return { transferTxHash };
}
-function tryGetMsgIdFromEvmTransferReceipt(receipt: providers.TransactionReceipt) {
- try {
- const messages = HyperlaneCore.getDispatchedMessages(receipt);
- if (messages.length) {
- const msgId = messages[0].id;
- logger.debug('Message id found in logs', msgId);
- return msgId;
- } else {
- logger.warn('No messages found in logs');
- return undefined;
- }
- } catch (error) {
- logger.error('Could not get msgId from transfer receipt', error);
- return undefined;
+async function executeIbcTransfer({
+ weiAmountOrId,
+ destinationDomainId,
+ recipientAddress,
+ tokenRoute,
+ activeChain,
+ activeAccountAddress,
+ updateStatus,
+ sendTransaction,
+}: ExecuteTransferParams) {
+ if (!isIbcRoute(tokenRoute)) throw new Error('Unsupported route type');
+ updateStatus(TransferStatus.CreatingTransfer);
+
+ const multiProvider = getMultiProvider();
+ const chainName = getChainMetadata(tokenRoute.originCaip2Id).name;
+ const adapterProperties = {
+ ibcDenom: tokenRoute.originIbcDenom,
+ sourcePort: tokenRoute.sourcePort,
+ sourceChannel: tokenRoute.sourceChannel,
+ };
+
+ let adapter: IHypTokenAdapter;
+ if (isIbcOnlyRoute(tokenRoute)) {
+ adapter = new CosmIbcTokenAdapter(chainName, multiProvider, {}, adapterProperties);
+ } else {
+ const intermediateChainName = getChainMetadata(tokenRoute.intermediateCaip2Id).name;
+ adapter = new CosmIbcToWarpTokenAdapter(
+ chainName,
+ multiProvider,
+ {
+ intermediateRouterAddress: tokenRoute.intermediateRouterAddress,
+ destinationRouterAddress: tokenRoute.destRouterAddress,
+ },
+ {
+ ...adapterProperties,
+ derivedIbcDenom: tokenRoute.derivedIbcDenom,
+ intermediateChainName,
+ },
+ );
}
+
+ const transferTxRequest = (await adapter.populateTransferRemoteTx({
+ weiAmountOrId,
+ recipient: recipientAddress,
+ fromAccountOwner: activeAccountAddress,
+ destination: destinationDomainId,
+ txValue: COSM_IGP_QUOTE,
+ })) as MsgTransferEncodeObject;
+
+ updateStatus(TransferStatus.SigningTransfer);
+ const { hash: transferTxHash, confirm: confirmTransfer } = await sendTransaction({
+ tx: { type: 'stargate', request: transferTxRequest },
+ chainCaip2Id: tokenRoute.originCaip2Id,
+ activeCap2Id: activeChain.chainCaip2Id,
+ });
+
+ updateStatus(TransferStatus.ConfirmingTransfer);
+ await confirmTransfer();
+
+ return { transferTxHash };
}
const errorMessages: Partial> = {
diff --git a/src/features/transfer/utils.ts b/src/features/transfer/utils.ts
new file mode 100644
index 00000000..7c447ef4
--- /dev/null
+++ b/src/features/transfer/utils.ts
@@ -0,0 +1,57 @@
+import { providers } from 'ethers';
+import { toast } from 'react-toastify';
+
+import { HyperlaneCore } from '@hyperlane-xyz/sdk';
+import { ProtocolType, convertDecimals } from '@hyperlane-xyz/utils';
+
+import { logger } from '../../utils/logger';
+import { getProtocolType } from '../caip/chains';
+import { isNonFungibleToken } from '../caip/tokens';
+import { AdapterFactory } from '../tokens/AdapterFactory';
+import { Route } from '../tokens/routes/types';
+import { isRouteToCollateral, isWarpRoute } from '../tokens/routes/utils';
+
+// In certain cases, like when a synthetic token has >1 collateral tokens
+// it's possible that the collateral contract balance is insufficient to
+// cover the remote transfer. This ensures the balance is sufficient or throws.
+export async function ensureSufficientCollateral(route: Route, weiAmount: string) {
+ if (!isRouteToCollateral(route) || isNonFungibleToken(route.baseTokenCaip19Id)) return;
+
+ // TODO cosmos support here
+ if (
+ getProtocolType(route.originCaip2Id) === ProtocolType.Cosmos ||
+ getProtocolType(route.destCaip2Id) === ProtocolType.Cosmos ||
+ !isWarpRoute(route)
+ )
+ return;
+
+ logger.debug('Ensuring collateral balance for route', route);
+ const adapter = AdapterFactory.HypTokenAdapterFromRouteDest(route);
+ const destinationBalance = await adapter.getBalance(route.destRouterAddress);
+ const destinationBalanceInOriginDecimals = convertDecimals(
+ route.destDecimals,
+ route.originDecimals,
+ destinationBalance,
+ );
+ if (destinationBalanceInOriginDecimals.lt(weiAmount)) {
+ toast.error('Collateral contract balance insufficient for transfer');
+ throw new Error('Insufficient collateral balance');
+ }
+}
+
+export function tryGetMsgIdFromEvmTransferReceipt(receipt: providers.TransactionReceipt) {
+ try {
+ const messages = HyperlaneCore.getDispatchedMessages(receipt);
+ if (messages.length) {
+ const msgId = messages[0].id;
+ logger.debug('Message id found in logs', msgId);
+ return msgId;
+ } else {
+ logger.warn('No messages found in logs');
+ return undefined;
+ }
+ } catch (error) {
+ logger.error('Could not get msgId from transfer receipt', error);
+ return undefined;
+ }
+}
diff --git a/src/features/wallet/CosmosWalletContext.tsx b/src/features/wallet/CosmosWalletContext.tsx
index b30c89fa..280af977 100644
--- a/src/features/wallet/CosmosWalletContext.tsx
+++ b/src/features/wallet/CosmosWalletContext.tsx
@@ -2,6 +2,7 @@ import { ChakraProvider, extendTheme } from '@chakra-ui/react';
import { GasPrice } from '@cosmjs/stargate';
import { wallets as cosmostationWallets } from '@cosmos-kit/cosmostation';
import { wallets as keplrWallets } from '@cosmos-kit/keplr';
+import { wallets as leapWallets } from '@cosmos-kit/leap';
import { ChainProvider } from '@cosmos-kit/react';
import '@interchain-ui/react/styles';
import { PropsWithChildren } from 'react';
@@ -26,7 +27,7 @@ export function CosmosWalletContext({ children }: PropsWithChildren) {
) {
gasPrice: GasPrice.fromString('0.025token'),
};
},
+ signingStargate: () => {
+ return {
+ // TODO cosmos get gas price from registry or RPC
+ gasPrice: GasPrice.fromString('0.2tia'),
+ };
+ },
}}
modalTheme={{ defaultTheme: 'light' }}
>
diff --git a/src/features/wallet/SideBarMenu.tsx b/src/features/wallet/SideBarMenu.tsx
index 3a382448..88774633 100644
--- a/src/features/wallet/SideBarMenu.tsx
+++ b/src/features/wallet/SideBarMenu.tsx
@@ -7,43 +7,23 @@ import { toTitleCase } from '@hyperlane-xyz/utils';
import { SmallSpinner } from '../../components/animation/SmallSpinner';
import { ChainLogo } from '../../components/icons/ChainLogo';
import { Identicon } from '../../components/icons/Identicon';
+import { PLACEHOLDER_COSMOS_CHAIN } from '../../consts/values';
import ArrowRightIcon from '../../images/icons/arrow-right.svg';
import CollapseIcon from '../../images/icons/collapse-icon.svg';
-import ConfirmedIcon from '../../images/icons/confirmed-icon.svg';
-import DeliveredIcon from '../../images/icons/delivered-icon.svg';
import Logout from '../../images/icons/logout.svg';
import ResetIcon from '../../images/icons/reset-icon.svg';
-import WarningIcon from '../../images/icons/transfer-warning-status.svg';
import Wallet from '../../images/icons/wallet.svg';
import { tryClipboardSet } from '../../utils/clipboard';
+import { STATUSES_WITH_ICON, getIconByTransferStatus } from '../../utils/transfer';
import { getAssetNamespace } from '../caip/tokens';
import { getChainDisplayName } from '../chains/utils';
import { useStore } from '../store';
import { getToken } from '../tokens/metadata';
import { TransfersDetailsModal } from '../transfer/TransfersDetailsModal';
-import { TransferContext, TransferStatus } from '../transfer/types';
+import { TransferContext } from '../transfer/types';
import { useAccounts, useDisconnectFns } from './hooks';
-const STATUSES_WITH_ICON = [
- TransferStatus.Delivered,
- TransferStatus.ConfirmedTransfer,
- TransferStatus.Failed,
-];
-
-const getIconByTransferStatus = (status: TransferStatus) => {
- switch (status) {
- case TransferStatus.Delivered:
- return DeliveredIcon;
- case TransferStatus.ConfirmedTransfer:
- return ConfirmedIcon;
- case TransferStatus.Failed:
- return WarningIcon;
- default:
- return WarningIcon;
- }
-};
-
export function SideBarMenu({
onConnectWallet,
isOpen,
@@ -116,25 +96,30 @@ export function SideBarMenu({
Connected Wallets