diff --git a/packages/web/components/bridge/immersive/amount-and-confirmation-screen.tsx b/packages/web/components/bridge/immersive/amount-and-confirmation-screen.tsx index 49e1ede849..9b08d98f9c 100644 --- a/packages/web/components/bridge/immersive/amount-and-confirmation-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-and-confirmation-screen.tsx @@ -1,6 +1,5 @@ import { CoinPretty } from "@keplr-wallet/unit"; import { BridgeChain } from "@osmosis-labs/bridge"; -import { MinimalAsset } from "@osmosis-labs/types"; import { isNil } from "@osmosis-labs/utils"; import { observer } from "mobx-react-lite"; import { useState } from "react"; @@ -35,7 +34,7 @@ export const AmountAndConfirmationScreen = observer( const { accountStore } = useStore(); const [sourceAsset, setSourceAsset] = useState(); - const [destinationAsset, setDestinationAsset] = useState(); + const [destinationAsset, setDestinationAsset] = useState(); const [fromChain, setFromChain] = useState(); const [toChain, setToChain] = useState(); @@ -43,41 +42,38 @@ export const AmountAndConfirmationScreen = observer( const [fiatAmount, setFiatAmount] = useState("0"); // Wallets - const destinationAccount = accountStore.getWallet( - accountStore.osmosisChainId - ); const { address: evmAddress } = useEvmWalletAccount(); - const sourceChain = direction === "deposit" ? fromChain : toChain; - const destinationChain = direction === "deposit" ? toChain : fromChain; - - const cosmosCounterpartyAccount = - sourceChain?.chainType === "evm" || isNil(sourceChain) + const fromChainCosmosAccount = + fromChain?.chainType === "evm" || isNil(fromChain) ? undefined - : accountStore.getWallet(sourceChain.chainId); + : accountStore.getWallet(fromChain.chainId); - const sourceAddress = - sourceChain?.chainType === "evm" - ? evmAddress - : cosmosCounterpartyAccount?.address; + const toChainCosmosAccount = + toChain?.chainType === "evm" || isNil(toChain) + ? undefined + : accountStore.getWallet(toChain.chainId); const quote = useBridgeQuote({ - destinationAddress: destinationAccount?.address, - destinationChain, - destinationAsset: destinationAsset - ? { - address: destinationAsset.coinMinimalDenom, - decimals: destinationAsset.coinDecimals, - denom: destinationAsset.coinDenom, - } - : undefined, - sourceAddress, - sourceChain, - sourceAsset, + toAddress: + toChain?.chainType === "evm" + ? evmAddress + : toChainCosmosAccount?.address, + toChain: toChain, + toAsset: destinationAsset, + fromAddress: + fromChain?.chainType === "evm" + ? evmAddress + : fromChainCosmosAccount?.address, + fromChain: fromChain, + fromAsset: sourceAsset, direction, onRequestClose: onClose, inputAmount: cryptoAmount, - bridges: sourceAsset?.supportedProviders, + bridges: + direction === "deposit" + ? sourceAsset?.supportedProviders + : destinationAsset?.supportedProviders, onTransfer: () => { setCryptoAmount("0"); setFiatAmount("0"); @@ -93,8 +89,6 @@ export const AmountAndConfirmationScreen = observer( void; toChain: BridgeChain | undefined; @@ -69,8 +65,8 @@ interface AmountScreenProps { sourceAsset: SupportedAssetWithAmount | undefined; setSourceAsset: (asset: SupportedAssetWithAmount | undefined) => void; - destinationAsset: MinimalAsset | undefined; - setDestinationAsset: (asset: MinimalAsset | undefined) => void; + destinationAsset: SupportedAsset | undefined; + setDestinationAsset: (asset: SupportedAsset | undefined) => void; cryptoAmount: string; fiatAmount: string; @@ -85,8 +81,6 @@ export const AmountScreen = observer( direction, selectedDenom, - sourceChain, - fromChain, setFromChain, toChain, @@ -105,6 +99,7 @@ export const AmountScreen = observer( quote, }: AmountScreenProps) => { + const { setCurrentScreen } = useScreenManager(); const { accountStore } = useStore(); const { onOpenWalletSelect } = useWalletSelect(); const { t } = useTranslation(); @@ -154,16 +149,16 @@ export const AmountScreen = observer( } = useEvmWalletAccount(); const cosmosCounterpartyAccountRepo = - sourceChain?.chainType === "evm" || isNil(sourceChain) + fromChain?.chainType === "evm" || isNil(fromChain) ? undefined - : accountStore.getWalletRepo(sourceChain.chainId); + : accountStore.getWalletRepo(fromChain.chainId); const cosmosCounterpartyAccount = - sourceChain?.chainType === "evm" || isNil(sourceChain) + fromChain?.chainType === "evm" || isNil(fromChain) ? undefined - : accountStore.getWallet(sourceChain.chainId); + : accountStore.getWallet(fromChain.chainId); const sourceAddress = - sourceChain?.chainType === "evm" + fromChain?.chainType === "evm" ? evmAddress : cosmosCounterpartyAccount?.address; @@ -199,17 +194,48 @@ export const AmountScreen = observer( canonicalAsset ); - const { supportedAssetsByChainId, supportedChains } = - useBridgesSupportedAssets({ - assets: assetsInOsmosis, - chain: { - chainId: accountStore.osmosisChainId, - chainType: "cosmos", - }, - }); + const { + supportedAssetsByChainId: counterpartySupportedAssetsByChainId, + supportedChains, + } = useBridgesSupportedAssets({ + assets: assetsInOsmosis, + chain: { + chainId: accountStore.osmosisChainId, + chainType: "cosmos", + }, + }); - const supportedAssets = - supportedAssetsByChainId[sourceChain?.chainId ?? ""]; + const supportedSourceAssets: SupportedAsset[] | undefined = useMemo(() => { + if (!fromChain) return undefined; + + // Use Osmosis Assets to get the source asset + if (direction === "withdraw") { + const selectedAsset = assetsInOsmosis?.find( + (asset) => asset.coinDenom === selectedDenom + ); + if (!selectedAsset) return undefined; + return [ + { + address: selectedAsset.coinMinimalDenom, + decimals: selectedAsset.coinDecimals, + chainId: fromChain.chainId, + chainType: fromChain.chainType, + denom: selectedAsset.coinDenom, + // Providers are not needed for withdrawals; they will be derived from the destinationAsset + supportedProviders: [], + supportedVariants: [selectedAsset.coinMinimalDenom], + }, + ]; + } + + return counterpartySupportedAssetsByChainId[fromChain.chainId]; + }, [ + assetsInOsmosis, + counterpartySupportedAssetsByChainId, + direction, + fromChain, + selectedDenom, + ]); const supportedChainsAsBridgeChain = useMemo( () => @@ -244,74 +270,119 @@ export const AmountScreen = observer( const hasMoreThanOneChainType = !isNil(firstSupportedCosmosChain) && !isNil(firstSupportedEvmChain); - const { - data: sourceAssetsBalances, - isLoading: isLoadingSourceAssetsBalance, - } = api.local.bridgeTransfer.getSupportedAssetsBalances.useQuery( - sourceChain?.chainType === "evm" - ? { - type: "evm", - assets: supportedAssets as Extract< - SupportedAsset, - { chainType: "evm" } - >[], - userEvmAddress: evmAddress, - } - : { - type: "cosmos", - assets: supportedAssets as Extract< - SupportedAsset, - { chainType: "cosmos" } - >[], - userCosmosAddress: cosmosCounterpartyAccount?.address, - }, - { - enabled: !isNil(sourceChain) && !isNil(supportedAssets), - - select: (data) => { - let nextData: typeof data = data; - - // Filter out assets with no balance - if (nextData) { - const filteredData = nextData.filter((asset) => - asset.amount.toDec().gt(new Dec(0)) - ); - - // If there are no assets with balance, leave one to be selected - if (filteredData.length === 0) { - nextData = [nextData[0]]; - } else { - nextData = filteredData; + const { data: assetsBalances, isLoading: isLoadingAssetsBalance } = + api.local.bridgeTransfer.getSupportedAssetsBalances.useQuery( + fromChain?.chainType === "evm" + ? { + type: "evm", + assets: supportedSourceAssets as Extract< + SupportedAsset, + { chainType: "evm" } + >[], + userEvmAddress: evmAddress, + } + : { + type: "cosmos", + assets: supportedSourceAssets as Extract< + SupportedAsset, + { chainType: "cosmos" } + >[], + userCosmosAddress: cosmosCounterpartyAccount?.address, + }, + { + enabled: !isNil(fromChain) && !isNil(supportedSourceAssets), + + select: (data) => { + let nextData: typeof data = data; + + // Filter out assets with no balance + if (nextData) { + const filteredData = nextData.filter((asset) => + asset.amount.toDec().gt(new Dec(0)) + ); + + // If there are no assets with balance, leave one to be selected + if (filteredData.length === 0) { + nextData = [nextData[0]]; + } else { + nextData = filteredData; + } } - } - if (!sourceAsset && nextData) { - const highestBalance = nextData.reduce( - (acc, curr) => - curr.amount.toDec().gt(acc.amount.toDec()) ? curr : acc, - nextData[0] - ); + if (!sourceAsset && nextData) { + const highestBalance = nextData.reduce( + (acc, curr) => + curr.amount.toDec().gt(acc.amount.toDec()) ? curr : acc, + nextData[0] + ); - setSourceAsset(highestBalance); - } + setSourceAsset(highestBalance); + } - return nextData; - }, - } - ); + return nextData; + }, + } + ); /** + * Deposit * Set the initial destination asset based on the source asset. */ useEffect(() => { - if (!isNil(sourceAsset) && !isNil(assetsInOsmosis)) { + if ( + direction === "deposit" && + !isNil(sourceAsset) && + !isNil(assetsInOsmosis) && + isNil(destinationAsset) + ) { const destinationAsset = assetsInOsmosis.find( (a) => a.coinMinimalDenom === sourceAsset.supportedVariants[0] )!; - setDestinationAsset(destinationAsset); + setDestinationAsset({ + address: destinationAsset.coinMinimalDenom, + decimals: destinationAsset.coinDecimals, + chainId: accountStore.osmosisChainId, + chainType: "cosmos", + denom: destinationAsset.coinDenom, + supportedProviders: sourceAsset.supportedProviders, + supportedVariants: [destinationAsset.coinMinimalDenom], + }); } - }, [assetsInOsmosis, setDestinationAsset, sourceAsset]); + }, [ + accountStore.osmosisChainId, + assetsInOsmosis, + destinationAsset, + direction, + setDestinationAsset, + sourceAsset, + ]); + + /** + * Withdraw + * Set the initial destination asset based on the source asset. + */ + useEffect(() => { + if ( + direction === "withdraw" && + isNil(destinationAsset) && + counterpartySupportedAssetsByChainId && + toChain + ) { + const counterpartyAssets = + counterpartySupportedAssetsByChainId[toChain.chainId]; + + if (counterpartyAssets && counterpartyAssets.length > 0) { + setDestinationAsset(counterpartyAssets[0]); + } + } + }, [ + counterpartySupportedAssetsByChainId, + destinationAsset, + direction, + setDestinationAsset, + toChain, + ]); /** * Set the osmosis chain based on the direction @@ -431,7 +502,7 @@ export const AmountScreen = observer( if ( isLoadingCanonicalAssetPrice || - isNil(supportedAssets) || + isNil(supportedSourceAssets) || !assetsInOsmosis || !canonicalAsset || !destinationAsset || @@ -521,13 +592,18 @@ export const AmountScreen = observer( ? t("transfer.deposit") : t("transfer.withdraw")} {" "} - token image{" "} - {canonicalAsset.coinDenom} +
@@ -577,19 +653,16 @@ export const AmountScreen = observer(
{inputUnit === "fiat" ? ( - <> - - + ) : (

@@ -641,7 +714,19 @@ export const AmountScreen = observer( -

@@ -649,63 +734,61 @@ export const AmountScreen = observer(
<> - {isLoadingSourceAssetsBalance && ( + {isLoadingAssetsBalance && (

Looking for balances

)} - {!isLoadingSourceAssetsBalance && - sourceAssetsBalances?.length === 1 && ( -
-

- {inputUnit === "crypto" - ? sourceAssetsBalances[0].amount - .trim(true) - .maxDecimals(6) - .hideDenom(true) - .toString() - : sourceAssetsBalances[0].usdValue.toString()}{" "} - {t("transfer.available")} -

-
- )} + {!isLoadingAssetsBalance && assetsBalances?.length === 1 && ( +
+

+ {inputUnit === "crypto" + ? assetsBalances[0].amount + .trim(true) + .maxDecimals(6) + .hideDenom(true) + .toString() + : assetsBalances[0].usdValue.toString()}{" "} + {t("transfer.available")} +

+
+ )} - {!isLoadingSourceAssetsBalance && - (sourceAssetsBalances?.length ?? 0) > 1 && ( -
- {(sourceAssetsBalances ?? []).map((asset) => { - const isActive = - asset.amount.currency.coinMinimalDenom === - sourceAsset?.address; - return ( - - ); - })} -
- )} + {!isLoadingAssetsBalance && (assetsBalances?.length ?? 0) > 1 && ( +
+ {(assetsBalances ?? []).map((asset) => { + const isActive = + asset.amount.currency.coinMinimalDenom === + sourceAsset?.address; + return ( + + ); + })} +
+ )} {walletConnected && ( @@ -724,12 +807,12 @@ export const AmountScreen = observer( @@ -775,12 +858,12 @@ export const AmountScreen = observer( - {destinationAsset?.coinDenom} + {destinationAsset?.denom} {sourceAsset.supportedVariants.map( (variantCoinMinimalDenom, index) => { + // TODO: HANDLE WITHDRAW CASE const asset = assetsInOsmosis.find( (asset) => asset.coinMinimalDenom === variantCoinMinimalDenom )!; const onClick = () => { - setDestinationAsset(asset); + setDestinationAsset({ + chainType: "cosmos", + address: asset.coinMinimalDenom, + decimals: asset.coinDecimals, + chainId: accountStore.osmosisChainId, + denom: asset.coinDenom, + supportedProviders: sourceAsset.supportedProviders, + supportedVariants: [asset.coinMinimalDenom], + }); }; // Show all as 'deposit as' for now @@ -852,7 +944,7 @@ export const AmountScreen = observer( false ?? asset.coinMinimalDenom === asset.variantGroupKey; const isSelected = - destinationAsset?.coinDenom === asset.coinDenom; + destinationAsset?.denom === asset.coinDenom; const isCanonicalAsset = index === 0; @@ -1067,7 +1159,7 @@ export const AmountScreen = observer( @@ -1075,7 +1167,7 @@ export const AmountScreen = observer( isNil(selectedQuote.gasCost) ? (
@@ -1168,6 +1260,11 @@ export const AmountScreen = observer( isLoadingBridgeTransaction } className="w-full text-h6 font-h6" + variant={ + warnUserOfSlippage || warnUserOfPriceImpact + ? "destructive" + : "default" + } > {buttonText} @@ -1185,11 +1282,7 @@ export const AmountScreen = observer( direction={direction} isOpen={areMoreOptionsVisible} fromAsset={sourceAsset} - toAsset={{ - address: destinationAsset.coinMinimalDenom, - decimals: destinationAsset.coinDecimals, - denom: destinationAsset.coinDenom, - }} + toAsset={destinationAsset} fromChain={fromChain} toChain={toChain} toAddress={sourceAddress} diff --git a/packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx b/packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx index 072cdbc84e..47f81629b2 100644 --- a/packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx +++ b/packages/web/components/bridge/immersive/bridge-provider-dropdown.tsx @@ -2,6 +2,7 @@ import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { Dec, PricePretty } from "@keplr-wallet/unit"; import { Bridge } from "@osmosis-labs/bridge"; import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; +import { isNil } from "@osmosis-labs/utils"; import classNames from "classnames"; import Image from "next/image"; import { useMemo } from "react"; @@ -24,16 +25,38 @@ export const BridgeProviderDropdown = ({ onSelect, }: Props) => { const { t } = useTranslation(); - const fastestQuote = useMemo( - () => - quotes.reduce((prev, current) => - prev.data.estimatedTime.asMilliseconds() < - current.data.estimatedTime.asMilliseconds() - ? prev - : current - ), - [quotes] - ); + const fastestQuote = useMemo(() => { + const minTime = Math.min( + ...quotes.map((q) => q.data.estimatedTime.asMilliseconds()) + ); + const uniqueFastestQuotes = quotes.filter( + (q) => q.data.estimatedTime.asMilliseconds() === minTime + ); + return uniqueFastestQuotes.length === 1 + ? uniqueFastestQuotes[0] + : undefined; + }, [quotes]); + + const cheapestQuote = useMemo(() => { + const minFee = quotes + .map((q) => q.data.transferFeeFiat?.toDec() ?? new Dec(0)) + .reduce((acc, fee) => { + if (acc === null || fee.lt(acc)) { + return fee; + } + return acc; + }, null as Dec | null); + + const uniqueCheapestQuotes = quotes.filter((q) => { + const feeDec = q.data.transferFeeFiat?.toDec(); + return !isNil(feeDec) && !isNil(minFee) && feeDec.equals(minFee); + }); + + return uniqueCheapestQuotes.length === 1 + ? uniqueCheapestQuotes[0] + : undefined; + }, [quotes]); + return ( {({ open }) => ( @@ -82,6 +105,11 @@ export const BridgeProviderDropdown = ({ ) .toString(); const isSelected = selectedQuote.provider.id === provider.id; + const isCheapest = + cheapestQuote?.data.provider.id === provider.id; + const isFastest = + fastestQuote?.data.provider.id === provider.id; + return (
diff --git a/packages/web/components/bridge/immersive/use-bridge-quote.ts b/packages/web/components/bridge/immersive/use-bridge-quote.ts index 362530dcc2..6ec7d177eb 100644 --- a/packages/web/components/bridge/immersive/use-bridge-quote.ts +++ b/packages/web/components/bridge/immersive/use-bridge-quote.ts @@ -31,13 +31,13 @@ export const useBridgeQuote = ({ inputAmount: inputAmountRaw, - sourceAddress, - sourceChain, - sourceAsset, + fromAddress, + fromChain, + fromAsset, - destinationAddress, - destinationAsset, - destinationChain, + toAddress, + toAsset, + toChain, bridges = ["Axelar", "Skip", "Squid", "IBC"], @@ -48,13 +48,13 @@ export const useBridgeQuote = ({ inputAmount: string; - sourceAsset: (BridgeAsset & { amount: CoinPretty }) | undefined; - sourceChain: BridgeChain | undefined; - sourceAddress: string | undefined; + fromAsset: (BridgeAsset & { amount: CoinPretty }) | undefined; + fromChain: BridgeChain | undefined; + fromAddress: string | undefined; - destinationAsset: BridgeAsset | undefined; - destinationChain: BridgeChain | undefined; - destinationAddress: string | undefined; + toAsset: BridgeAsset | undefined; + toChain: BridgeChain | undefined; + toAddress: string | undefined; bridges?: Bridge[]; @@ -72,19 +72,6 @@ export const useBridgeQuote = ({ useSendEvmTransaction(); const { t } = useTranslation(); - // In the context of Osmosis, this refers to the Osmosis chain. - const destinationPath = { - address: destinationAddress, - asset: destinationAsset, - chain: destinationChain, - }; - - const sourcePath = { - address: sourceAddress, - asset: sourceAsset, - chain: sourceChain, - }; - const isDeposit = direction === "deposit"; const isWithdraw = direction === "withdraw"; @@ -93,14 +80,17 @@ export const useBridgeQuote = ({ RouterInputs["bridgeTransfer"]["getQuoteByBridge"], "bridge" | "fromAmount" > - > = { - fromAddress: isDeposit ? sourcePath.address : destinationPath.address, - fromAsset: isDeposit ? sourcePath.asset : destinationPath.asset, - fromChain: isDeposit ? sourcePath.chain : destinationPath.chain, - toAddress: isDeposit ? destinationPath.address : sourcePath.address, - toAsset: isDeposit ? destinationPath.asset : sourcePath.asset, - toChain: isDeposit ? destinationPath.chain : sourcePath.chain, - }; + > = useMemo( + () => ({ + fromAddress, + fromAsset, + fromChain, + toAddress, + toAsset, + toChain, + }), + [fromAddress, fromAsset, fromChain, toAddress, toAsset, toChain] + ); const [selectedBridgeProvider, setSelectedBridgeProvider] = useState(null); @@ -129,10 +119,10 @@ export const useBridgeQuote = ({ debouncedInputValue === "" ? "0" : debouncedInputValue ).mul( // CoinPretty only accepts whole amounts - DecUtils.getTenExponentNInPrecisionRange(destinationAsset?.decimals ?? 0) + DecUtils.getTenExponentNInPrecisionRange(toAsset?.decimals ?? 0) ); - const availableBalance = sourceAsset?.amount; + const availableBalance = fromAsset?.amount; const isInsufficientBal = inputAmountRaw !== "" && @@ -396,13 +386,13 @@ export const useBridgeQuote = ({ .trim(true) .toString(), isWithdraw, - destinationAddress ?? "" // use osmosis account (destinationAddress) for account keys (vs any EVM account) + toAddress ?? "" // use osmosis account (destinationAddress) for account keys (vs any EVM account) ); } }, [ availableBalance, - destinationAddress, + toAddress, inputAmount, inputAmountRaw, isWithdraw, @@ -413,10 +403,9 @@ export const useBridgeQuote = ({ const [isApprovingToken, setIsApprovingToken] = useState(false); const isSendTxPending = (() => { - if (!destinationChain) return false; - return destinationChain.chainType === "cosmos" - ? accountStore.getWallet(destinationChain.chainId)?.txTypeInProgress !== - "" + if (!toChain) return false; + return toChain.chainType === "cosmos" + ? accountStore.getWallet(toChain.chainId)?.txTypeInProgress !== "" : isEthTxPending; })(); @@ -503,13 +492,13 @@ export const useBridgeQuote = ({ const handleCosmosTx = async ( quote: NonNullable["quote"] ) => { - if (!destinationChain || destinationChain?.chainType !== "cosmos") { + if (!toChain || toChain?.chainType !== "cosmos") { throw new Error("Destination chain is not cosmos"); } const transactionRequest = quote.transactionRequest as CosmosBridgeTransactionRequest; return accountStore.signAndBroadcast( - destinationChain.chainId, + toChain.chainId, transactionRequest.msgTypeUrl, [ { @@ -522,12 +511,12 @@ export const useBridgeQuote = ({ undefined, (tx: DeliverTxResponse) => { if (tx.code == null || tx.code === 0) { - const queries = queriesStore.get(destinationChain.chainId); + const queries = queriesStore.get(toChain.chainId); // After succeeding to send token, refresh the balance. const queryBalance = queries.queryBalances // If we get here destination address is defined - .getQueryBech32Address(destinationAddress!) + .getQueryBech32Address(toAddress!) .balances.find((bal) => { return ( bal.currency.coinMinimalDenom === @@ -576,12 +565,12 @@ export const useBridgeQuote = ({ const warnUserOfSlippage = selectedQuote?.isSlippageTooHigh; const warnUserOfPriceImpact = selectedQuote?.isPriceImpactTooHigh; const isCorrectEvmChainSelected = - sourceChain?.chainType === "evm" - ? currentEvmChainId === sourceChain?.chainId + fromChain?.chainType === "evm" + ? currentEvmChainId === fromChain?.chainId : true; let buttonErrorMessage: string | undefined; - if (!sourceAddress) { + if (!fromAddress) { buttonErrorMessage = t("assets.transfer.errors.missingAddress"); } else if (hasNoQuotes) { buttonErrorMessage = t("assets.transfer.errors.noQuotesAvailable");