From 3c5451d7579029fb8b6265600d77a64055ff4289 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Thu, 4 Jul 2024 21:48:36 -0400 Subject: [PATCH 01/10] feat: add send address modal --- packages/utils/package.json | 3 +- packages/utils/src/string.ts | 25 + .../immersive/amount-and-review-screen.tsx | 11 +- .../bridge/immersive/amount-screen.tsx | 237 +++++----- .../immersive/bridge-network-select-modal.tsx | 12 +- .../immersive/bridge-wallet-select-modal.tsx | 441 ++++++++++++------ .../bridge/immersive/crypto-fiat-input.tsx | 67 ++- .../bridge/immersive/review-screen.tsx | 2 +- packages/web/components/input/index.ts | 1 + .../web/components/input/textarea-box.tsx | 115 +++++ .../use-connect-wallet-modal-redirect.tsx | 6 +- packages/web/hooks/use-wallet-select.tsx | 2 + packages/web/localizations/en.json | 10 +- .../web/server/api/routers/bridge-transfer.ts | 6 +- packages/web/tailwind.config.js | 1 + yarn.lock | 135 +----- 16 files changed, 653 insertions(+), 421 deletions(-) create mode 100644 packages/web/components/input/textarea-box.tsx diff --git a/packages/utils/package.json b/packages/utils/package.json index f8f3cc0b07..1b405747eb 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -17,7 +17,8 @@ "@keplr-wallet/unit": "0.10.24-ibc.go.v7.hot.fix", "@osmosis-labs/types": "^1.0.0", "sha.js": "^2.4.11", - "viem": "2.16.4" + "viem": "2.16.4", + "@cosmjs/encoding": "0.32.3" }, "devDependencies": { "@types/jest-in-case": "^1.0.6", diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts index b120035932..c87e7b76a7 100644 --- a/packages/utils/src/string.ts +++ b/packages/utils/src/string.ts @@ -1,3 +1,6 @@ +import * as cosmjsEncoding from "@cosmjs/encoding"; +import * as viem from "viem"; + /** Trucates a string with ellipsis, default breakpoint: `num = 8`. */ export function truncateString(str: string, num = 8) { if (str.length <= num) { @@ -58,3 +61,25 @@ export const ellipsisText = (str: string, maxLength: number): string => { export const camelCaseToSnakeCase = (input: string) => { return input.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase(); }; + +export function isEvmAddressValid({ address }: { address: string }): boolean { + return viem.isAddress(address); +} + +export function isCosmosAddressValid({ + address, + bech32Prefix, +}: { + address: string; + bech32Prefix: string; +}): boolean { + try { + const { prefix, data } = cosmjsEncoding.fromBech32(address); + if (prefix !== bech32Prefix) { + return false; + } + return data.length === 20; + } catch { + return false; + } +} diff --git a/packages/web/components/bridge/immersive/amount-and-review-screen.tsx b/packages/web/components/bridge/immersive/amount-and-review-screen.tsx index 0f20d0e8f5..48f6b4f36e 100644 --- a/packages/web/components/bridge/immersive/amount-and-review-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-and-review-screen.tsx @@ -39,6 +39,8 @@ export const AmountAndReviewScreen = observer( const [cryptoAmount, setCryptoAmount] = useState("0"); const [fiatAmount, setFiatAmount] = useState("0"); + const [manualToAddress, setManualToAddress] = useState(); + // Wallets const { address: evmAddress, connector: evmConnector } = useEvmWalletAccount(); @@ -57,8 +59,11 @@ export const AmountAndReviewScreen = observer( fromChain?.chainType === "evm" ? evmAddress : fromChainCosmosAccount?.address; - const toAddress = - toChain?.chainType === "evm" ? evmAddress : toChainCosmosAccount?.address; + const toAddress = !isNil(manualToAddress) + ? manualToAddress + : toChain?.chainType === "evm" + ? evmAddress + : toChainCosmosAccount?.address; const fromWalletIcon = fromChain?.chainType === "evm" @@ -121,6 +126,8 @@ export const AmountAndReviewScreen = observer( setFromChain={setFromChain} toChain={toChain} setToChain={setToChain} + manualToAddress={manualToAddress} + setManualToAddress={setManualToAddress} fromAsset={fromAsset} setFromAsset={setFromAsset} toAsset={toAsset} diff --git a/packages/web/components/bridge/immersive/amount-screen.tsx b/packages/web/components/bridge/immersive/amount-screen.tsx index a9d637a7a3..c6cfd294f8 100644 --- a/packages/web/components/bridge/immersive/amount-screen.tsx +++ b/packages/web/components/bridge/immersive/amount-screen.tsx @@ -16,6 +16,7 @@ import Image from "next/image"; import { FunctionComponent, ReactNode, + useCallback, useEffect, useMemo, useState, @@ -74,6 +75,9 @@ interface AmountScreenProps { toAsset: SupportedAsset | undefined; setToAsset: (asset: SupportedAsset | undefined) => void; + manualToAddress: string | undefined; + setManualToAddress: (address: string | undefined) => void; + cryptoAmount: string; fiatAmount: string; setCryptoAmount: (amount: string) => void; @@ -99,6 +103,9 @@ export const AmountScreen = observer( toAsset, setToAsset, + manualToAddress, + setManualToAddress, + cryptoAmount, setCryptoAmount, fiatAmount, @@ -125,14 +132,6 @@ export const AmountScreen = observer( warnUserOfSlippage, } = quote; - const { accountActionButton: connectWalletButton, walletConnected } = - useConnectWalletModalRedirect( - { - className: "w-full", - }, - noop - ); - const [areMoreOptionsVisible, setAreMoreOptionsVisible] = useState(false); const [inputUnit, setInputUnit] = useState<"crypto" | "fiat">("fiat"); @@ -148,21 +147,14 @@ export const AmountScreen = observer( address: evmAddress, connector: evmConnector, isConnected: isEvmWalletConnected, + isConnecting, } = useEvmWalletAccount(); - const fromCosmosCounterpartyAccountRepo = - fromChain?.chainType === "evm" || isNil(fromChain) - ? undefined - : accountStore.getWalletRepo(fromChain.chainId); const fromCosmosCounterpartyAccount = fromChain?.chainType === "evm" || isNil(fromChain) ? undefined : accountStore.getWallet(fromChain.chainId); - const toCosmosCounterpartyAccountRepo = - toChain?.chainType === "evm" || isNil(toChain) - ? undefined - : accountStore.getWalletRepo(toChain.chainId); const toCosmosCounterpartyAccount = toChain?.chainType === "evm" || isNil(toChain) ? undefined @@ -199,8 +191,6 @@ export const AmountScreen = observer( * Use the canonical osmosis asset to determine the price of the assets. * This is because providers can return variant assets that are missing in * our asset list. - * - * TODO: Weigh the pros and cons of filtering variant assets not in our asset list. */ canonicalAsset ); @@ -216,6 +206,96 @@ export const AmountScreen = observer( }, }); + const firstSupportedEvmChain = useMemo( + () => + supportedChains.find( + ( + chain + ): chain is Extract< + BridgeChainWithDisplayInfo, + { chainType: "evm" } + > => chain.chainType === "evm" + ), + [supportedChains] + ); + const firstSupportedCosmosChain = useMemo( + () => + supportedChains.find( + ( + chain + ): chain is Extract< + BridgeChainWithDisplayInfo, + { chainType: "cosmos" } + > => chain.chainType === "cosmos" + ), + [supportedChains] + ); + + const hasMoreThanOneChainType = + !isNil(firstSupportedCosmosChain) && !isNil(firstSupportedEvmChain); + + const checkChainAndConnectWallet = useCallback( + (chainParam?: BridgeChainWithDisplayInfo) => { + const chain = + chainParam ?? (direction === "deposit" ? fromChain : toChain); + + if (!chain) return; + if (chain.chainType === "evm") { + // TODO: Fix dead-end when the user disconnects the wallet in manage + if (isEvmWalletConnected || isConnecting) { + return; + } + + onOpenBridgeWalletSelect(); + } else if (chain.chainType === "cosmos") { + const account = accountStore.getWallet(chain.chainId); + const accountRepo = accountStore.getWalletRepo(chain.chainId); + + if ( + // If the account is already connected + !!account?.address || + // Or if the repo is already connected + !!accountRepo?.current + ) { + return; + } + + accountRepo?.connect(osmosisAccount?.walletName).catch(() => + // Display the connect modal if the user for some reason rejects the connection + // TODO: Open the network select modal + onOpenWalletSelect({ + walletOptions: [ + { walletType: "cosmos", chainId: String(chain.chainId) }, + ], + }) + ); + } + }, + [ + accountStore, + direction, + fromChain, + isConnecting, + isEvmWalletConnected, + onOpenBridgeWalletSelect, + onOpenWalletSelect, + osmosisAccount?.walletName, + toChain, + ] + ); + + const { accountActionButton: connectWalletButton, walletConnected } = + useConnectWalletModalRedirect( + { + className: "w-full", + }, + noop, + undefined, + () => { + checkChainAndConnectWallet(); + } + ); + const supportedSourceAssets: SupportedAsset[] | undefined = useMemo(() => { if (!fromChain) return undefined; @@ -248,34 +328,6 @@ export const AmountScreen = observer( selectedDenom, ]); - const firstSupportedEvmChain = useMemo( - () => - supportedChains.find( - ( - chain - ): chain is Extract< - BridgeChainWithDisplayInfo, - { chainType: "evm" } - > => chain.chainType === "evm" - ), - [supportedChains] - ); - const firstSupportedCosmosChain = useMemo( - () => - supportedChains.find( - ( - chain - ): chain is Extract< - BridgeChainWithDisplayInfo, - { chainType: "cosmos" } - > => chain.chainType === "cosmos" - ), - [supportedChains] - ); - - const hasMoreThanOneChainType = - !isNil(firstSupportedCosmosChain) && !isNil(firstSupportedEvmChain); - const { data: assetsBalances, isLoading: isLoadingAssetsBalance } = api.local.bridgeTransfer.getSupportedAssetsBalances.useQuery( fromChain?.chainType === "evm" @@ -405,6 +457,7 @@ export const AmountScreen = observer( chainType: "cosmos", logoUri: osmosisChain.logoURIs?.svg ?? osmosisChain.logoURIs?.png, color: osmosisChain.logoURIs?.theme?.primary_color_hex, + bech32Prefix: osmosisChain.bech32_prefix, }); } }, [direction, fromChain, osmosisChain, setFromChain, setToChain, toChain]); @@ -424,94 +477,17 @@ export const AmountScreen = observer( ) { const firstChain = supportedChains[0]; setChain(firstChain); + checkChainAndConnectWallet(firstChain); } }, [ + checkChainAndConnectWallet, direction, fromChain, setFromChain, setToChain, supportedChains, toChain, - ]); - - /** - * Connect cosmos wallet to the counterparty chain - */ - useEffect(() => { - if (!fromChain || !toChain) return; - - const account = - direction === "deposit" - ? fromCosmosCounterpartyAccount - : toCosmosCounterpartyAccount; - const accountRepo = - direction === "deposit" - ? fromCosmosCounterpartyAccountRepo - : toCosmosCounterpartyAccountRepo; - const chain = direction === "deposit" ? fromChain : toChain; - - if ( - // If the chain is an EVM chain, we don't need to connect the cosmos chain - chain.chainType !== "cosmos" || - // Or if the account is already connected - !!account?.address || - // Or if there's no available cosmos chain - !firstSupportedCosmosChain || - // Or if the account is already connected - !!accountRepo?.current - ) { - return; - } - - accountRepo?.connect(osmosisAccount?.walletName).catch(() => - // Display the connect modal if the user for some reason rejects the connection - onOpenWalletSelect({ - walletOptions: [ - { walletType: "cosmos", chainId: String(chain.chainId) }, - ], - }) - ); - }, [ - direction, - firstSupportedCosmosChain, - fromChain, - fromCosmosCounterpartyAccount, - fromCosmosCounterpartyAccountRepo, - onOpenWalletSelect, - osmosisAccount?.walletName, - toChain, - toCosmosCounterpartyAccount, - toCosmosCounterpartyAccountRepo, - ]); - - /** - * Connect evm wallet to the counterparty chain - */ - useEffect(() => { - if (!fromChain || !toChain) return; - - const chain = direction === "deposit" ? fromChain : toChain; - - if ( - // If the chain is an Cosmos chain, we don't need to connect the cosmos chain - chain.chainType !== "evm" || - // Or if the account is already connected - isEvmWalletConnected || - // Or if there's no available evm chain - !firstSupportedEvmChain - ) { - return; - } - - onOpenBridgeWalletSelect(); - }, [ - direction, - evmAddress, - firstSupportedEvmChain, - fromChain, - isEvmWalletConnected, - onOpenBridgeWalletSelect, - toChain, + walletConnected, ]); if ( @@ -583,9 +559,11 @@ export const AmountScreen = observer( chainColor={fromChain.color} chainLogo={fromChain.logoUri} chains={supportedChains} + toChain={toChain} onSelectChain={(nextChain) => { setFromChain(nextChain); resetAssets(); + if (walletConnected) checkChainAndConnectWallet(nextChain); if (fromChain?.chainId !== nextChain.chainId) { resetInput(); } @@ -602,9 +580,11 @@ export const AmountScreen = observer( chainColor={toChain.color} chainLogo={toChain.logoUri} chains={supportedChains} + toChain={toChain} onSelectChain={(nextChain) => { setToChain(nextChain); resetAssets(); + if (walletConnected) checkChainAndConnectWallet(nextChain); if (fromChain?.chainId !== nextChain.chainId) { resetInput(); } @@ -629,6 +609,7 @@ export const AmountScreen = observer( setFiatAmount={setFiatAmount} setCryptoAmount={setCryptoAmount} setInputUnit={setInputUnit} + fromChain={fromChain} /> <> @@ -753,6 +734,7 @@ export const AmountScreen = observer( ? chain : firstSupportedCosmosChain; })()} + toChain={toChain} /> ) : ( @@ -1048,6 +1030,7 @@ const ChainSelectorButton: FunctionComponent<{ chainLogo: string | undefined; chainColor: string | undefined; chains: ReturnType["supportedChains"]; + toChain: BridgeChainWithDisplayInfo; onSelectChain: (chain: BridgeChainWithDisplayInfo) => void; }> = ({ direction, @@ -1057,6 +1040,7 @@ const ChainSelectorButton: FunctionComponent<{ chainColor, chains, onSelectChain, + toChain, }) => { const [isNetworkSelectVisible, setIsNetworkSelectVisible] = useState(false); @@ -1103,6 +1087,7 @@ const ChainSelectorButton: FunctionComponent<{ }} onRequestClose={() => setIsNetworkSelectVisible(false)} direction={direction} + toChain={toChain} /> )} diff --git a/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx b/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx index 7ca1897180..6d6323e2db 100644 --- a/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx +++ b/packages/web/components/bridge/immersive/bridge-network-select-modal.tsx @@ -24,6 +24,7 @@ enum Screens { } interface BridgeNetworkSelectModalProps extends ModalBaseProps { direction: BridgeTransactionDirection; + toChain: BridgeChainWithDisplayInfo; chains: ReturnType["supportedChains"]; onSelectChain: (chain: BridgeChainWithDisplayInfo) => void; } @@ -32,6 +33,7 @@ export const BridgeNetworkSelectModal = ({ direction, chains, onSelectChain, + toChain, ...modalProps }: BridgeNetworkSelectModalProps) => { const { t } = useTranslation(); @@ -57,8 +59,10 @@ export const BridgeNetworkSelectModal = ({ }, [chains, query]); return ( - - {({ currentScreen, setCurrentScreen }) => ( + + {({ currentScreen }) => ( <> { setQuery(""); - setCurrentScreen(Screens.Main); + setConnectingToEvmChain(undefined); }} > @@ -92,6 +96,7 @@ export const BridgeNetworkSelectModal = ({ onSelectChain(chain); }} evmChain={connectingToEvmChain} + toChain={toChain} /> @@ -154,7 +159,6 @@ export const BridgeNetworkSelectModal = ({ ...chain, chainId: Number(chain.chainId), }); - setCurrentScreen(Screens.SelectWallet); return; } diff --git a/packages/web/components/bridge/immersive/bridge-wallet-select-modal.tsx b/packages/web/components/bridge/immersive/bridge-wallet-select-modal.tsx index 2744ddc549..55d25f2a48 100644 --- a/packages/web/components/bridge/immersive/bridge-wallet-select-modal.tsx +++ b/packages/web/components/bridge/immersive/bridge-wallet-select-modal.tsx @@ -1,14 +1,27 @@ import { BridgeTransactionDirection } from "@osmosis-labs/types"; -import { isNil } from "@osmosis-labs/utils"; +import { + isCosmosAddressValid, + isEvmAddressValid, + isNil, +} from "@osmosis-labs/utils"; import classNames from "classnames"; import { observer } from "mobx-react-lite"; import React, { ReactNode, useState } from "react"; import { Connector } from "wagmi"; -import { SearchBox } from "~/components/input"; +import { Icon } from "~/components/assets"; +import { ChainLogo } from "~/components/assets/chain-logo"; +import { SearchBox, TextareaBox } from "~/components/input"; +import { + Screen, + ScreenGoBackButton, + ScreenManager, +} from "~/components/screen-manager"; import { Button } from "~/components/ui/button"; +import { Checkbox } from "~/components/ui/checkbox"; import { SwitchingNetworkState } from "~/components/wallet-states/switching-network-state"; import { EthereumChainIds } from "~/config/wagmi"; +import { useTranslation } from "~/hooks"; import { useDisconnectEvmWallet, useEvmWalletAccount, @@ -20,9 +33,9 @@ import { useConnectWallet } from "~/modals/wallet-select/use-connect-wallet"; import { useSelectableWallets } from "~/modals/wallet-select/use-selectable-wallets"; import { BridgeChainWithDisplayInfo } from "~/server/api/routers/bridge-transfer"; import { useStore } from "~/stores"; - interface BridgeWalletSelectProps extends ModalBaseProps { direction: BridgeTransactionDirection; + toChain: BridgeChainWithDisplayInfo; cosmosChain?: Extract; evmChain?: Extract; onSelectChain: (chain: BridgeChainWithDisplayInfo) => void; @@ -30,8 +43,14 @@ interface BridgeWalletSelectProps extends ModalBaseProps { export const BridgeWalletSelectModal = observer( (props: BridgeWalletSelectProps) => { - const { direction, cosmosChain, evmChain, onSelectChain, ...modalProps } = - props; + const { + direction, + cosmosChain, + evmChain, + onSelectChain, + toChain, + ...modalProps + } = props; return ( ); } ); +const enum WalletSelectScreens { + WalletSelect = "wallet-select", + SendToAnotherAddress = "send-to-another-address", +} + export const BridgeWalletSelectScreen = ({ + direction, cosmosChain, evmChain, onClose, onSelectChain, + toChain, }: Pick< BridgeWalletSelectProps, - "cosmosChain" | "evmChain" | "direction" | "onSelectChain" + "cosmosChain" | "evmChain" | "direction" | "onSelectChain" | "toChain" > & { onClose: () => void; }) => { @@ -143,144 +170,194 @@ export const BridgeWalletSelectScreen = ({ const showEvmWallets = !isNil(evmChain) && !isNil(evmWallets); return ( -
-
- {showEvmWallets && ( - { - setSearch(nextValue); - }} - currentValue={search} - /> - )} + + {({ setCurrentScreen }) => ( + <> + + { + setCurrentScreen(WalletSelectScreens.WalletSelect); + }} + /> + {}} toChain={toChain} /> + - {(isEvmWalletConnected || !isNil(cosmosAccount)) && ( -
-
-

Your connected wallets

- -
+ +
+
+ {showEvmWallets && ( + { + setSearch(nextValue); + }} + currentValue={search} + /> + )} - <> - {!isNil(cosmosAccount) && !isNil(cosmosChain) && ( - { - onSelectChain(cosmosChain); - onClose(); - }} - name={ - isManaging ? ( - cosmosAccount.walletInfo.prettyName - ) : ( - <>Transfer from {cosmosAccount?.walletInfo.prettyName} - ) - } - icon={cosmosAccount.walletInfo.logo} - suffix={ - isManaging ? ( -

Primary wallet

- ) : undefined - } - /> - )} - {isEvmWalletConnected && !isNil(evmChain) && ( - { - const shouldSwitchChain = - isEvmWalletConnected && - currentEvmChainId !== evmChain.chainId; +
+ {(isEvmWalletConnected || !isNil(cosmosAccount)) && ( +
+
+

+ Your connected wallets +

+ +
- if (shouldSwitchChain) { - try { - setIsSwitchingChain(true); - await switchChainAsync({ - chainId: evmChain.chainId as EthereumChainIds, - }); - } catch { - setIsSwitchingChain(false); - return; - } - } + {!isNil(cosmosAccount) && !isNil(cosmosChain) && ( + { + onSelectChain(cosmosChain); + onClose(); + }} + name={ + isManaging ? ( + cosmosAccount.walletInfo.prettyName + ) : ( + <> + Transfer from{" "} + {cosmosAccount?.walletInfo.prettyName} + + ) + } + icon={cosmosAccount.walletInfo.logo} + suffix={ + isManaging ? ( +

+ Primary wallet +

+ ) : undefined + } + /> + )} + {isEvmWalletConnected && !isNil(evmChain) && ( + { + const shouldSwitchChain = + isEvmWalletConnected && + currentEvmChainId !== evmChain.chainId; - onSelectChain(evmChain); - onClose(); - }} - name={ - isManaging ? ( - evmConnector?.name - ) : ( - <>Transfer from {evmConnector?.name ?? ""} - ) - } - icon={evmConnector?.icon} - suffix={ - isManaging ? ( - - ) : undefined - } - /> - )} - -
- )} + if (shouldSwitchChain) { + try { + setIsSwitchingChain(true); + await switchChainAsync({ + chainId: evmChain.chainId as EthereumChainIds, + }); + } catch { + setIsSwitchingChain(false); + return; + } + } - {showEvmWallets && ( -
-

Other wallets

-
- {evmWallets - .filter((wallet) => { - if (wallet.id === evmConnector?.id) return false; // Don't show connected wallet - if (!search) return true; - return wallet.name - .toLowerCase() - .includes(search.toLowerCase()); - }) - .map((wallet) => { - return ( + onSelectChain(evmChain); + onClose(); + }} + name={ + isManaging ? ( + evmConnector?.name + ) : ( + <>Transfer from {evmConnector?.name ?? ""} + ) + } + icon={evmConnector?.icon} + suffix={ + isManaging ? ( + + ) : undefined + } + /> + )} +
+ )} + {direction === "withdraw" && ( - onConnectWallet({ - walletType: "evm", - wallet, - chainId: evmChain.chainId as EthereumChainIds, - }) + onClick={() => { + setCurrentScreen( + WalletSelectScreens.SendToAnotherAddress + ); + }} + name={"Send to another address"} + icon={ +
+ +
+ } + suffix={ + } - name={wallet.name} - icon={wallet.icon} /> - ); - })} -
-
- )} -
-
+ )} +
+ + {showEvmWallets && ( +
+

+ Other wallets +

+
+ {evmWallets + .filter((wallet) => { + if (wallet.id === evmConnector?.id) return false; // Don't show connected wallet + if (!search) return true; + return wallet.name + .toLowerCase() + .includes(search.toLowerCase()); + }) + .map((wallet) => { + return ( + + onConnectWallet({ + walletType: "evm", + wallet, + chainId: evmChain.chainId as EthereumChainIds, + }) + } + name={wallet.name} + icon={wallet.icon} + /> + ); + })} +
+
+ )} +
+
+ + + )} +
); }; const WalletButton: React.FC<{ onClick: () => void; - icon: string | undefined; + icon: ReactNode | undefined; name: ReactNode; suffix?: ReactNode; }> = ({ onClick, icon, name, suffix }) => { @@ -295,8 +372,10 @@ const WalletButton: React.FC<{ onClick={onClick} > @@ -305,3 +384,101 @@ const WalletButton: React.FC<{ ); }; + +interface SendToAnotherAddressFormProps { + onConfirm: (address: string) => void; + toChain: BridgeChainWithDisplayInfo; +} + +const SendToAnotherAddressForm = ({ + onConfirm, + toChain, +}: SendToAnotherAddressFormProps) => { + const { t } = useTranslation(); + const [isInvalidAddress, setIsInvalidAddress] = useState(false); + const [address, setAddress] = useState(""); + const [isAcknowledged, setIsAcknowledged] = useState(false); + + const handleConfirm = () => { + if (isAcknowledged) { + onConfirm(address); + } + }; + + return ( +
+
+ +

+ {t("transfer.verifyAddressWarning")} +

+
+
+ + { + const isValid = + toChain.chainType === "cosmos" + ? isCosmosAddressValid({ + address: nextValue, + bech32Prefix: toChain.bech32Prefix, + }) + : toChain.chainType === "evm" && + isEvmAddressValid({ address: nextValue }); + + if (!nextValue) setIsInvalidAddress(false); + else setIsInvalidAddress(!isValid); + + setAddress(nextValue); + }} + placeholder={t("transfer.enterAddress")} + className="w-full" + classes={{ + textarea: isInvalidAddress ? "text-rust-200" : undefined, + }} + trailingSymbol={ + + } + rows={2} + /> + {isInvalidAddress && ( +

+ {t("transfer.invalidAddress", { chain: toChain.prettyName })} +

+ )} +
+
+ setIsAcknowledged(!isAcknowledged)} + /> +

+ {t("transfer.acknowledgement")} +

+
+ +
+ ); +}; diff --git a/packages/web/components/bridge/immersive/crypto-fiat-input.tsx b/packages/web/components/bridge/immersive/crypto-fiat-input.tsx index 042ad6a6ff..384e376470 100644 --- a/packages/web/components/bridge/immersive/crypto-fiat-input.tsx +++ b/packages/web/components/bridge/immersive/crypto-fiat-input.tsx @@ -12,7 +12,9 @@ import { import { Icon } from "~/components/assets"; import { InputBox } from "~/components/input"; +import { Tooltip } from "~/components/tooltip"; import { useTranslation } from "~/hooks"; +import { BridgeChainWithDisplayInfo } from "~/server/api/routers/bridge-transfer"; import { trimPlaceholderZeros } from "~/utils/number"; import { SupportedAssetWithAmount } from "./amount-and-review-screen"; @@ -25,6 +27,7 @@ export const CryptoFiatInput: FunctionComponent<{ asset: SupportedAssetWithAmount; isInsufficientBal: boolean; isInsufficientFee: boolean; + fromChain: BridgeChainWithDisplayInfo; transferGasCost: CoinPretty | undefined; setFiatAmount: (amount: string) => void; setCryptoAmount: (amount: string) => void; @@ -35,6 +38,7 @@ export const CryptoFiatInput: FunctionComponent<{ fiatInputRaw, assetPrice, asset, + fromChain, isInsufficientBal, isInsufficientFee, transferGasCost, @@ -46,6 +50,7 @@ export const CryptoFiatInput: FunctionComponent<{ const inputRef = useRef(null); const [isMax, setIsMax] = useState(false); + const [hasSubtractedAmount, setHasSubtractedAmount] = useState(false); const inputCoin = useMemo( () => @@ -120,6 +125,7 @@ export const CryptoFiatInput: FunctionComponent<{ if (inputCoin.toDec().gt(maxTransferAmount)) { onInput("crypto")(trimPlaceholderZeros(maxTransferAmount.toString())); + setHasSubtractedAmount(true); } } }, [isMax, transferGasCost, asset.amount, inputCoin, onInput]); @@ -172,6 +178,7 @@ export const CryptoFiatInput: FunctionComponent<{ onInput={(value) => { onInput("fiat")(value); setIsMax(false); + setHasSubtractedAmount(false); }} isAutosize /> @@ -203,33 +210,53 @@ export const CryptoFiatInput: FunctionComponent<{ onInput={(value) => { onInput("crypto")(value); setIsMax(false); + setHasSubtractedAmount(false); }} trailingSymbol={inputCoin.denom} isAutosize /> )} - + +