- {onChangeShielded && chain?.chain_name.includes("namada") && (
-
- )}
- {onToggleCustomAddress && (
-
- )}
- {!customAddressActive && (
-
-
- {!walletAddress && (
-
- )}
- {wallet && walletAddress && (
-
- )}
+ <>
+
+ {!isSubmitting && (
+
+
+
+ Destination
+
+ {isShieldedTransfer ||
+ (isShieldingTransfer && (
+
+

+
+
+ ))}
- )}
-
- {customAddressActive && (
-
- {onToggleCustomAddress && (
-
- )}
-
-
- )}
-
- )}
- {isSubmitting && amount && destinationAsset && (
-
-
-
- )}
+
+ {!destinationAddress ?
+
+
+
+
+ Select address
+
+
+
+
+
+ :
-
- )}
+ )}
+
+ {isSubmitting && amount && destinationAsset && (
+
+
+
+ )}
+
+ {isSubmitting && (
+
+ )}
+
+ {!isSubmitting && (
+
+ )}
+
- {!isSubmitting && (
-
+ {isModalOpen && (
+
)}
-
+ >
);
};
diff --git a/apps/namadillo/src/App/Transfer/TransferLayout.tsx b/apps/namadillo/src/App/Transfer/TransferLayout.tsx
index 63c574772b..9f8831e32e 100644
--- a/apps/namadillo/src/App/Transfer/TransferLayout.tsx
+++ b/apps/namadillo/src/App/Transfer/TransferLayout.tsx
@@ -1,29 +1,184 @@
+import { Panel } from "@namada/components";
import { NavigationFooter } from "App/AccountOverview/NavigationFooter";
import { ConnectPanel } from "App/Common/ConnectPanel";
import { PageWithSidebar } from "App/Common/PageWithSidebar";
+import { IbcTransfer } from "App/Ibc/IbcTransfer";
+import { IbcWithdraw } from "App/Ibc/IbcWithdraw";
+import { LearnAboutIbc } from "App/Ibc/LearnAboutIbc";
import { Sidebar } from "App/Layout/Sidebar";
+import { LearnAboutMasp } from "App/Masp/LearnAboutMasp";
+import { MaspShield } from "App/Masp/MaspShield";
+import { MaspUnshield } from "App/Masp/MaspUnshield";
import { LearnAboutTransfer } from "App/NamadaTransfer/LearnAboutTransfer";
+import { NamadaTransfer } from "App/NamadaTransfer/NamadaTransfer";
+import { MaspAssetRewards } from "App/Sidebars/MaspAssetRewards";
+import { allDefaultAccountsAtom } from "atoms/accounts";
+import { shieldedBalanceAtom } from "atoms/balance";
+import { applicationFeaturesAtom } from "atoms/settings";
import { useUserHasAccount } from "hooks/useIsAuthenticated";
-import { Outlet } from "react-router-dom";
+import { useUrlState } from "hooks/useUrlState";
+import { KeplrWalletManager } from "integrations/Keplr";
+import { useAtomValue } from "jotai";
+import { useEffect, useState } from "react";
+import { isTransparentAddress } from ".";
+import { determineTransferType } from "./utils";
export const TransferLayout: React.FC = () => {
+ const keplrWalletManager = new KeplrWalletManager();
const userHasAccount = useUserHasAccount();
+ const features = useAtomValue(applicationFeaturesAtom);
+ const [sourceAddress, setSourceAddress] = useUrlState("source");
+ const [destinationAddress, setDestinationAddress] =
+ useUrlState("destination");
+ const [assetSelectorModalOpen, setAssetSelectorModalOpen] = useState(false);
+
+ const { refetch: refetchShieldedBalance } = useAtomValue(shieldedBalanceAtom);
+ const { data: accounts } = useAtomValue(allDefaultAccountsAtom);
+
+ const transferType = determineTransferType({
+ sourceAddress,
+ destinationAddress,
+ });
+
+ const transparentAddress =
+ accounts?.find((acc) => isTransparentAddress(acc.address))?.address ?? "";
+
+ // Initialize source address
+ useEffect(() => {
+ if (!sourceAddress && transparentAddress) {
+ setSourceAddress(transparentAddress);
+ }
+ }, [transparentAddress]);
+
+ // Refetch shielded balance for MASP operations
+ useEffect(() => {
+ if (transferType === "shield" || transferType === "unshield") {
+ refetchShieldedBalance();
+ }
+ }, [transferType, refetchShieldedBalance]);
if (!userHasAccount) {
- return
;
+ let actionText = "To transfer assets";
+ switch (transferType) {
+ case "shield":
+ case "unshield":
+ actionText = "To shield assets";
+ break;
+ case "ibc-deposit":
+ case "ibc-withdraw":
+ actionText = "To IBC Transfer";
+ break;
+ }
+ return
;
}
+ const renderContent = (): JSX.Element => {
+ if (transferType === "ibc-deposit") {
+ return (
+
+
+
+ );
+ }
+
+ if (transferType === "ibc-withdraw") {
+ return (
+
+
+
+ );
+ }
+
+ if (transferType === "shield") {
+ return (
+
+
+
+ );
+ }
+
+ if (transferType === "unshield") {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+ };
+
+ // Render sidebar based on transfer type
+ const renderSidebar = (): JSX.Element => {
+ const isIbcTransfer =
+ transferType === "ibc-deposit" || transferType === "ibc-withdraw";
+ const isMaspTransfer =
+ transferType === "shield" || transferType === "unshield";
+
+ if (isIbcTransfer) {
+ return
;
+ }
+
+ if (isMaspTransfer) {
+ return (
+ <>
+ {features.shieldingRewardsEnabled &&
}
+
+ >
+ );
+ }
+
+ return
;
+ };
+
return (
-
-
-
+
{renderContent()}
-
-
-
+ {renderSidebar()}
);
};
diff --git a/apps/namadillo/src/App/Transfer/TransferModule.tsx b/apps/namadillo/src/App/Transfer/TransferModule.tsx
index 2d24ca9079..92363d0670 100644
--- a/apps/namadillo/src/App/Transfer/TransferModule.tsx
+++ b/apps/namadillo/src/App/Transfer/TransferModule.tsx
@@ -1,223 +1,128 @@
-import { Chain } from "@chain-registry/types";
import { ActionButton, Stack } from "@namada/components";
-import { mapUndefined } from "@namada/utils";
import { IconTooltip } from "App/Common/IconTooltip";
import { InlineError } from "App/Common/InlineError";
-import { routes } from "App/routes";
+import { params, routes } from "App/routes";
+import {
+ namadaShieldedAssetsAtom,
+ namadaTransparentAssetsAtom,
+} from "atoms/balance";
import { namadaRegistryChainAssetsMapAtom } from "atoms/integrations";
import BigNumber from "bignumber.js";
import clsx from "clsx";
+import { useAssetsWithAmounts } from "hooks/useAssetsWithAmounts";
import { useKeychainVersion } from "hooks/useKeychainVersion";
-import { TransactionFeeProps } from "hooks/useTransactionFee";
-import { wallets } from "integrations";
import { useAtomValue } from "jotai";
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { useEffect, useMemo } from "react";
import { BsQuestionCircleFill } from "react-icons/bs";
-import { Link, useLocation, useNavigate } from "react-router-dom";
import {
- Address,
- AssetWithAmount,
- BaseDenom,
- GasConfig,
- LedgerAccountInfo,
- WalletProvider,
-} from "types";
+ Link,
+ useLocation,
+ useNavigate,
+ useSearchParams,
+} from "react-router-dom";
+import { AssetWithAmount } from "types";
import { filterAvailableAssetsWithBalance } from "utils/assets";
-import { checkKeychainCompatibleWithMasp } from "utils/compatibility";
import { getDisplayGasFee } from "utils/gas";
-import {
- isShieldedAddress,
- isTransparentAddress,
- parseChainInfo,
-} from "./common";
+import { isIbcAddress, isShieldedAddress } from "./common";
import { CurrentStatus } from "./CurrentStatus";
import { IbcChannels } from "./IbcChannels";
-import { SelectAssetModal } from "./SelectAssetModal";
-import { SelectChainModal } from "./SelectChainModal";
-import { SelectWalletModal } from "./SelectWalletModal";
+import { SelectToken } from "./SelectToken";
import { SuccessAnimation } from "./SuccessAnimation";
import { TransferArrow } from "./TransferArrow";
import { TransferDestination } from "./TransferDestination";
import { TransferSource } from "./TransferSource";
-
-type TransferModuleConfig = {
- wallet?: WalletProvider;
- walletAddress?: string;
- availableWallets?: WalletProvider[];
- connected?: boolean;
- availableChains?: Chain[];
- chain?: Chain;
- isShieldedAddress?: boolean;
- onChangeWallet?: (wallet: WalletProvider) => void;
- onChangeChain?: (chain: Chain) => void;
- onChangeShielded?: (isShielded: boolean) => void;
- isSyncingMasp?: boolean;
- // Additional information if selected account is a ledger
- ledgerAccountInfo?: LedgerAccountInfo;
-};
-
-export type TransferSourceProps = TransferModuleConfig & {
- availableAssets?: Record
;
- isLoadingAssets?: boolean;
- selectedAssetAddress?: Address;
- availableAmount?: BigNumber;
- onChangeSelectedAsset?: (address: Address | undefined) => void;
- amount?: BigNumber;
- onChangeAmount?: (amount: BigNumber | undefined) => void;
-};
-
-export type IbcOptions = {
- sourceChannel: string;
- onChangeSourceChannel: (channel: string) => void;
- destinationChannel?: string;
- onChangeDestinationChannel?: (channel: string) => void;
-};
-
-export type TransferDestinationProps = TransferModuleConfig & {
- enableCustomAddress?: boolean;
- customAddress?: Address;
- onChangeCustomAddress?: (address: Address) => void;
- onChangeShielded?: (shielded: boolean) => void;
-};
-
-export type OnSubmitTransferParams = {
- displayAmount: BigNumber;
- destinationAddress: Address;
- memo?: string;
-};
-
-export type TransferModuleProps = {
- source: TransferSourceProps;
- destination: TransferDestinationProps;
- onSubmitTransfer?: (params: OnSubmitTransferParams) => void;
- requiresIbcChannels?: boolean;
- gasConfig?: GasConfig;
- feeProps?: TransactionFeeProps;
- changeFeeEnabled?: boolean;
- submittingText?: string;
- isSubmitting?: boolean;
- errorMessage?: string;
- currentStatus?: string;
- currentStatusExplanation?: string;
- completedAt?: Date;
- isShieldedTx?: boolean;
- isSyncingMasp?: boolean;
- buttonTextErrors?: Partial>;
- onComplete?: () => void;
-} & (
- | { ibcTransfer?: undefined; ibcOptions?: undefined }
- | { ibcTransfer: "deposit" | "withdraw"; ibcOptions: IbcOptions }
-);
-
-type ValidationResult =
- | "NoAmount"
- | "NoSourceWallet"
- | "NoSourceChain"
- | "NoSelectedAsset"
- | "NoDestinationWallet"
- | "NoDestinationChain"
- | "NoTransactionFee"
- | "NotEnoughBalance"
- | "NotEnoughBalanceForFees"
- | "KeychainNotCompatibleWithMasp"
- | "CustomAddressNotMatchingChain"
- | "TheSameAddress"
- | "NoLedgerConnected"
- | "Ok";
-
-// Check if the provided address is valid for the destination chain and transaction type
-const isValidDestinationAddress = ({
- customAddress,
- chain,
-}: {
- customAddress: string;
- chain: Chain | undefined;
-}): boolean => {
- // Skip validation if no custom address or chain provided
- if (!customAddress || !chain || !chain.bech32_prefix) return true;
-
- // Check shielded/transparent address requirements for Namada
- if (chain.bech32_prefix === "tnam") {
- return (
- isTransparentAddress(customAddress) || isShieldedAddress(customAddress)
- );
- }
-
- // For non-Namada chains, validate using prefix
- return customAddress.startsWith(chain.bech32_prefix);
-};
+import { TransferModuleProps, ValidationResult } from "./types";
+import { getButtonText, validateTransferForm } from "./utils";
export const TransferModule = ({
source,
destination,
- gasConfig: gasConfigProp,
feeProps,
- changeFeeEnabled,
- submittingText,
isSubmitting,
- ibcTransfer,
- ibcOptions,
- requiresIbcChannels,
- onSubmitTransfer,
errorMessage,
currentStatus,
currentStatusExplanation,
+ gasConfig: gasConfigProp,
+ onSubmitTransfer,
completedAt,
onComplete,
- buttonTextErrors = {},
- isSyncingMasp = false,
- isShieldedTx = false,
+ ibcChannels,
+ requiresIbcChannels,
+ keplrWalletManager,
+ assetSelectorModalOpen = false,
+ setAssetSelectorModalOpen = () => {},
}: TransferModuleProps): JSX.Element => {
+ const { data: usersAssets, isLoading: isLoadingUsersAssets } = useAtomValue(
+ isShieldedAddress(source.address ?? "") ?
+ namadaShieldedAssetsAtom
+ : namadaTransparentAssetsAtom
+ );
+ const [searchParams, setSearchParams] = useSearchParams();
+ const asset = searchParams.get(params.asset) || "";
+ const assetsWithAmounts = useAssetsWithAmounts(source.address ?? "");
+ const selectedAsset =
+ source.selectedAssetWithAmount ??
+ assetsWithAmounts.find(
+ (assetWithAmount) => assetWithAmount.asset.address === asset
+ );
+
+ const availableAmount = selectedAsset?.amount;
+ const availableAssets = useMemo(() => {
+ return filterAvailableAssetsWithBalance(usersAssets);
+ }, [usersAssets]);
+ const chainAssets = useAtomValue(namadaRegistryChainAssetsMapAtom);
+
const navigate = useNavigate();
const location = useLocation();
- const [walletSelectorModalOpen, setWalletSelectorModalOpen] = useState(false);
- const [sourceChainModalOpen, setSourceChainModalOpen] = useState(false);
- const [destinationChainModalOpen, setDestinationChainModalOpen] =
- useState(false);
- const [assetSelectorModalOpen, setAssetSelectorModalOpen] = useState(false);
- const [customAddressActive, setCustomAddressActive] = useState(
- destination.enableCustomAddress && !destination.availableWallets
- );
- const [memo, setMemo] = useState();
const keychainVersion = useKeychainVersion();
- const chainAssetsMap = useAtomValue(namadaRegistryChainAssetsMapAtom);
+ const isTargetShielded = isShieldedAddress(destination.address ?? "");
+ const isSourceShielded = isShieldedAddress(source.address ?? "");
+ const isShielding =
+ isShieldedAddress(destination.address ?? "") &&
+ !isShieldedAddress(source.address ?? "");
+ const isUnshielding =
+ isShieldedAddress(source.address ?? "") &&
+ !isShieldedAddress(destination.address ?? "");
+ const isShieldedTx = isShieldedAddress(source.address ?? "");
+ const buttonColor = isTargetShielded || isSourceShielded ? "yellow" : "white";
+ const ibcTransfer =
+ isIbcAddress(destination.address ?? "") ||
+ isIbcAddress(source.address ?? "");
+
+ const getButtonTextFromValidation = (): string => {
+ const buttonTextErrors =
+ isShielding || isUnshielding ?
+ {
+ NoAmount:
+ isShielding ? "Define an amount to shield"
+ : isUnshielding ? "Define an amount to unshield"
+ : "",
+ }
+ : {};
+
+ return getButtonText({
+ isSubmitting,
+ validationResult,
+ availableAmountMinusFees,
+ buttonTextErrors,
+ });
+ };
- const chainAssets = Object.values(chainAssetsMap.data || {}) ?? [];
const gasConfig = gasConfigProp ?? feeProps?.gasConfig;
const displayGasFee = useMemo(() => {
return gasConfig ?
- getDisplayGasFee(gasConfig, chainAssetsMap.data || {})
+ getDisplayGasFee(gasConfig, chainAssets.data ?? {})
: undefined;
}, [gasConfig]);
- const availableAssets: Record =
- useMemo(() => {
- return filterAvailableAssetsWithBalance(source.availableAssets);
- }, [source.availableAssets]);
-
- const firstAvailableAsset = Object.values(availableAssets)[0];
-
- const selectedAsset = mapUndefined(
- (address) => source.availableAssets?.[address],
- source.selectedAssetAddress
- );
-
const availableAmountMinusFees = useMemo(() => {
- const { selectedAssetAddress, availableAmount } = source;
-
- if (
- typeof selectedAssetAddress === "undefined" ||
- typeof availableAmount === "undefined" ||
- typeof availableAssets === "undefined"
- ) {
+ if (!selectedAsset?.asset.address || !availableAmount || !availableAssets)
return undefined;
- }
-
if (
!displayGasFee?.totalDisplayAmount ||
// Don't subtract if the gas token is different than the selected asset:
- gasConfig?.gasToken !== selectedAssetAddress
+ gasConfig?.gasToken !== selectedAsset?.asset.address
) {
return availableAmount;
}
@@ -227,250 +132,56 @@ export const TransferModule = ({
.decimalPlaces(6);
return BigNumber.max(amountMinusFees, 0);
- }, [source.selectedAssetAddress, source.availableAmount, displayGasFee]);
+ }, [selectedAsset?.asset.address, availableAmount, displayGasFee]);
const validationResult = useMemo((): ValidationResult => {
- if (!source.wallet) {
- return "NoSourceWallet";
- } else if (source.walletAddress === destination.customAddress) {
- return "TheSameAddress";
- } else if (
- !isValidDestinationAddress({
- customAddress: destination.customAddress ?? "",
- chain: destination.chain,
- })
- ) {
- return "CustomAddressNotMatchingChain";
- } else if (
- (source.isShieldedAddress || destination.isShieldedAddress) &&
- keychainVersion &&
- !checkKeychainCompatibleWithMasp(keychainVersion)
- ) {
- return "KeychainNotCompatibleWithMasp";
- } else if (!source.chain) {
- return "NoSourceChain";
- } else if (!destination.chain) {
- return "NoDestinationChain";
- } else if (!source.selectedAssetAddress) {
- return "NoSelectedAsset";
- } else if (!hasEnoughBalanceForFees()) {
- return "NotEnoughBalanceForFees";
- } else if (!source.amount || source.amount.eq(0)) {
- return "NoAmount";
- } else if (
- !availableAmountMinusFees ||
- source.amount.gt(availableAmountMinusFees)
- ) {
- return "NotEnoughBalance";
- } else if (!destination.wallet && !destination.customAddress) {
- return "NoDestinationWallet";
- } else if (
- (source.isShieldedAddress || destination.isShieldedAddress) &&
- source.ledgerAccountInfo &&
- !source.ledgerAccountInfo.deviceConnected
- ) {
- return "NoLedgerConnected";
- } else {
- return "Ok";
- }
- }, [source, destination, gasConfig, availableAmountMinusFees]);
+ return validateTransferForm({
+ source: {
+ assetAddress: selectedAsset?.asset.address,
+ address: source.address,
+ isShieldedAddress: isShieldedAddress(source.address ?? ""),
+ selectedAssetAddress: selectedAsset?.asset.address,
+ amount: source.amount,
+ ledgerAccountInfo: source.ledgerAccountInfo,
+ },
+ destination: {
+ address: destination.address,
+ isShieldedAddress: isShieldedAddress(destination.address ?? ""),
+ },
+ gasConfig,
+ availableAmountMinusFees,
+ keychainVersion,
+ availableAssets,
+ displayGasFeeAmount: displayGasFee?.totalDisplayAmount,
+ });
+ }, [
+ source.address,
+ selectedAsset?.asset.address,
+ source.amount,
+ source.ledgerAccountInfo,
+ gasConfig,
+ availableAmountMinusFees,
+ keychainVersion,
+ availableAssets,
+ displayGasFee,
+ ]);
const onSubmit = (e: React.FormEvent): void => {
e.preventDefault();
- const address = destination.customAddress || destination.walletAddress;
- if (!source.amount) {
- throw new Error("Amount is not valid");
- }
-
- if (!address) {
- throw new Error("Address is not provided");
- }
-
- if (!source.selectedAssetAddress) {
- throw new Error("Asset is not selected");
- }
-
- const params: OnSubmitTransferParams = {
- displayAmount: source.amount,
- destinationAddress: address.trim(),
- memo,
- };
-
- onSubmitTransfer?.(params);
- };
-
- const onChangeWallet = (config: TransferModuleConfig) => (): void => {
- // No callback available, do nothing
- if (!config.onChangeWallet) return;
-
- // User may choose between multiple options
- if ((config.availableWallets || []).length > 1) {
- setWalletSelectorModalOpen(true);
- return;
- }
-
- // Fallback to default wallet prop
- if (!config.availableWallets && config.wallet) {
- config.onChangeWallet(config.wallet);
- return;
- }
-
- // Do nothing if no alternatives are provided
- if (!config.availableWallets) {
- return;
- }
-
- // Do nothing if wallet address is set, and no other wallet is available
- if (config.walletAddress && config.availableWallets.length <= 1) {
- return;
- }
-
- setWalletSelectorModalOpen(true);
- };
-
- function hasEnoughBalanceForFees(): boolean {
- // Skip if transaction fees will be handled by another wallet, like Keplr.
- // (Ex: when users transfer from IBC to Namada)
- if (source.wallet && source.wallet !== wallets.namada) {
- return true;
- }
-
- if (!availableAssets || !gasConfig || !displayGasFee) {
- return false;
- }
-
- // Find how much the user has in their account for the selected fee token
- const feeTokenAddress = gasConfig.gasToken;
-
- if (!availableAssets.hasOwnProperty(feeTokenAddress)) {
- return false;
- }
-
- const assetDisplayAmount = availableAssets[feeTokenAddress].amount;
- const feeDisplayAmount = displayGasFee?.totalDisplayAmount;
-
- return assetDisplayAmount.gt(feeDisplayAmount);
- }
-
- const filteredAvailableAssets = useMemo(() => {
- // Get available assets that are accepted by the chain
- return Object.values(availableAssets).filter(({ asset }) => {
- if (!source.chain) return true;
- return chainAssets.some(
- (chainAsset) =>
- chainAsset?.symbol.toLowerCase() === asset?.symbol.toLowerCase()
- );
+ onSubmitTransfer({
+ displayAmount: source.amount?.toString() ?? "",
+ destinationAddress: destination.address,
+ sourceAddress: source.address,
+ memo: destination.memo,
});
- }, [availableAssets, source.chain, chainAssets]);
-
- const sortedAssets = useMemo(() => {
- if (!filteredAvailableAssets.length) {
- return [];
- }
-
- // Sort filtered assets by amount
- return [...filteredAvailableAssets].sort(
- (asset1: AssetWithAmount, asset2: AssetWithAmount) => {
- return asset1.amount.gt(asset2.amount) ? -1 : 1;
- }
- );
- }, [filteredAvailableAssets]);
-
- const getButtonTextError = (
- id: ValidationResult,
- defaultText: string
- ): string => {
- if (buttonTextErrors.hasOwnProperty(id) && buttonTextErrors[id]) {
- return buttonTextErrors[id];
- }
-
- return defaultText;
- };
-
- const getButtonText = (): string | JSX.Element => {
- if (isSubmitting) {
- return submittingText || "Submitting...";
- }
-
- const getText = getButtonTextError.bind(null, validationResult);
- switch (validationResult) {
- case "NoSourceWallet":
- return getText("Select Wallet");
- case "TheSameAddress":
- return getText("Source and destination addresses are the same");
-
- case "NoSourceChain":
- case "NoDestinationChain":
- return getText("Select Chain");
-
- case "NoSelectedAsset":
- return getText("Select Asset");
- case "NoDestinationWallet":
- return getText("Select Destination Wallet");
-
- case "NoAmount":
- return getText("Define an amount to transfer");
-
- case "NoTransactionFee":
- return getText("No transaction fee is set");
-
- case "CustomAddressNotMatchingChain":
- return getText("Custom address does not match chain");
- case "NotEnoughBalance":
- return getText("Not enough balance");
- case "NotEnoughBalanceForFees":
- return getText("Not enough balance to pay for transaction fees");
- case "KeychainNotCompatibleWithMasp":
- return getText("Keychain is not compatible with MASP");
- case "NoLedgerConnected":
- return getText("Connect your ledger and open the Namada App");
- }
-
- if (!availableAmountMinusFees) {
- return getText("Wallet amount not available");
- }
-
- return "Submit";
};
- const buttonColor =
- destination.isShieldedAddress || source.isShieldedAddress ?
- "yellow"
- : "white";
-
- const renderLedgerTooltip = useCallback(
- () => (
- }
- text={
-
- If your device is connected and the app is open, please go to{" "}
- {
- e.preventDefault();
- navigate(routes.settingsLedger, {
- state: { backgroundLocation: location },
- });
- }}
- to={routes.settingsLedger}
- className="text-yellow"
- >
- Settings
- {" "}
- and pair your device with Namadillo.
-
- }
- />
- ),
- []
- );
-
+ // Set the selected asset in the parent component from the URL state if it's not set
useEffect(() => {
- if (!selectedAsset?.asset && firstAvailableAsset) {
- source.onChangeSelectedAsset?.(firstAvailableAsset?.asset.address);
+ if (!source.selectedAssetWithAmount && selectedAsset) {
+ source.onChangeSelectedAsset(selectedAsset);
}
- }, [firstAvailableAsset]);
+ }, [selectedAsset]);
return (
<>
@@ -484,85 +195,55 @@ export const TransferModule = ({
onSubmit={onSubmit}
>
setSourceChainModalOpen(true)
- : undefined
- }
openAssetSelector={
- source.onChangeSelectedAsset && !isSubmitting ?
- () => setAssetSelectorModalOpen(true)
- : undefined
+ !isSubmitting ? () => setAssetSelectorModalOpen(true) : undefined
}
onChangeAmount={source.onChangeAmount}
- isShieldedAddress={source.isShieldedAddress}
- onChangeShielded={source.onChangeShielded}
isSubmitting={isSubmitting}
/>
- destination.onChangeWallet && destination.wallet ?
- destination.onChangeWallet(destination.wallet)
- : undefined
- }
- openChainSelector={
- destination.onChangeChain && !isSubmitting ?
- () => setDestinationChainModalOpen(true)
- : undefined
- }
- onChangeAddress={destination.onChangeCustomAddress}
- memo={memo}
- onChangeMemo={setMemo}
+ destinationAddress={destination.address}
+ sourceAsset={selectedAsset?.asset}
+ sourceAddress={source.address}
+ onChangeAddress={destination.onChangeAddress}
+ memo={destination.memo}
+ onChangeMemo={destination.onChangeMemo}
feeProps={feeProps}
- changeFeeEnabled={changeFeeEnabled}
gasDisplayAmount={displayGasFee?.totalDisplayAmount}
gasAsset={displayGasFee?.asset}
destinationAsset={selectedAsset?.asset}
amount={source.amount}
isSubmitting={isSubmitting}
/>
- {ibcTransfer && requiresIbcChannels && (
+ {ibcTransfer && requiresIbcChannels && ibcChannels && (
)}
{!isSubmitting && }
@@ -572,7 +253,7 @@ export const TransferModule = ({
explanation={currentStatusExplanation}
/>
)}
- {!isSubmitting && onSubmitTransfer && (
+ {!isSubmitting && (
- {getButtonText()}
+ {getButtonTextFromValidation()}
-
- {validationResult === "NoLedgerConnected" &&
- renderLedgerTooltip()}
+ {validationResult === "NoLedgerConnected" && (
+
+ }
+ text={
+
+ If your device is connected and the app is open, please go
+ to{" "}
+ {
+ e.preventDefault();
+ navigate(routes.settingsLedger, {
+ state: { backgroundLocation: location },
+ });
+ }}
+ to={routes.settingsLedger}
+ className="text-yellow"
+ >
+ Settings
+ {" "}
+ and pair your device with Namadillo.
+
+ }
+ />
+ )}
)}
{validationResult === "KeychainNotCompatibleWithMasp" && (
@@ -604,54 +309,35 @@ export const TransferModule = ({
/>
)}
-
- {walletSelectorModalOpen &&
- source.onChangeWallet &&
- source.availableWallets && (
- setWalletSelectorModalOpen(false)}
- onConnect={source.onChangeWallet}
- />
- )}
-
- {assetSelectorModalOpen &&
- source.onChangeSelectedAsset &&
- source.wallet &&
- source.walletAddress && (
- setAssetSelectorModalOpen(false)}
- assets={sortedAssets.map(
- (assetsWithAmount) => assetsWithAmount.asset
- )}
- onSelect={source.onChangeSelectedAsset}
- wallet={source.wallet}
- walletAddress={source.walletAddress}
- ibcTransfer={ibcTransfer}
- />
- )}
-
- {sourceChainModalOpen && source.onChangeChain && source.wallet && (
- setSourceChainModalOpen(false)}
- chains={source.availableChains || []}
- onSelect={source.onChangeChain}
- wallet={source.wallet}
- walletAddress={source.walletAddress}
- />
- )}
-
- {destinationChainModalOpen &&
- destination.onChangeChain &&
- destination.wallet && (
- setDestinationChainModalOpen(false)}
- chains={destination.availableChains || []}
- onSelect={destination.onChangeChain}
- wallet={destination.wallet}
- walletAddress={destination.walletAddress}
- />
- )}
+ setAssetSelectorModalOpen(false)}
+ assetsWithAmounts={assetsWithAmounts}
+ onSelect={(
+ selectedAssetWithAmount: AssetWithAmount,
+ newSourceAddress?: string
+ ) => {
+ // Batch both URL updates together
+ setSearchParams((prev) => {
+ const newParams = new URLSearchParams(prev);
+ if (newSourceAddress) {
+ newParams.set("source", newSourceAddress);
+ }
+ newParams.set(
+ params.asset,
+ selectedAssetWithAmount.asset.address || ""
+ );
+ return newParams;
+ });
+ source.onChangeAmount(undefined);
+ source.onChangeSelectedAsset(selectedAssetWithAmount);
+ setAssetSelectorModalOpen(false);
+ }}
+ />
>
);
};
diff --git a/apps/namadillo/src/App/Transfer/TransferSource.tsx b/apps/namadillo/src/App/Transfer/TransferSource.tsx
index 32cbc49c96..e52ef11abc 100644
--- a/apps/namadillo/src/App/Transfer/TransferSource.tsx
+++ b/apps/namadillo/src/App/Transfer/TransferSource.tsx
@@ -1,35 +1,30 @@
-import { Chain } from "@chain-registry/types";
-import { AmountInput } from "@namada/components";
-import { TabSelector } from "App/Common/TabSelector";
-import { MaspSyncIndicator } from "App/Layout/MaspSyncIndicator";
+import { Asset } from "@chain-registry/types";
+import { AmountInput, Tooltip } from "@namada/components";
import BigNumber from "bignumber.js";
import clsx from "clsx";
-import { Asset, WalletProvider } from "types";
+import { wallets } from "integrations";
+import { Address } from "types";
+import namadaShieldedIcon from "./assets/namada-shielded.svg";
+import namadaTransparentIcon from "./assets/namada-transparent.svg";
import { AvailableAmountFooter } from "./AvailableAmountFooter";
-import { ConnectProviderButton } from "./ConnectProviderButton";
+import { isShieldedAddress, isTransparentAddress } from "./common";
import { SelectedAsset } from "./SelectedAsset";
-import { SelectedChain } from "./SelectedChain";
-import { SelectedWallet } from "./SelectedWallet";
import { TokenAmountCard } from "./TokenAmountCard";
export type TransferSourceProps = {
- isConnected: boolean;
- wallet?: WalletProvider;
- walletAddress?: string;
- asset?: Asset;
isLoadingAssets?: boolean;
isSubmitting?: boolean;
- isSyncingMasp?: boolean;
- chain?: Chain;
- openChainSelector?: () => void;
- openAssetSelector?: () => void;
- openProviderSelector?: () => void;
- amount?: BigNumber;
+ isShieldingTxn?: boolean;
+ asset?: Asset;
+ originalAddress?: Address;
+ sourceAddress?: string;
availableAmount?: BigNumber;
availableAmountMinusFees?: BigNumber;
+ amount?: BigNumber;
+ selectedTokenType?: "shielded" | "transparent" | "keplr";
+ openAssetSelector?: () => void;
+ openProviderSelector?: () => void;
onChangeAmount?: (amount: BigNumber | undefined) => void;
- isShieldedAddress?: boolean;
- onChangeShielded?: (isShielded: boolean) => void;
};
const amountMaxDecimalPlaces = (asset?: Asset): number | undefined => {
@@ -43,111 +38,94 @@ const amountMaxDecimalPlaces = (asset?: Asset): number | undefined => {
return undefined;
};
+const getWalletIcon = (
+ sourceAddress?: string,
+ selectedTokenType?: "shielded" | "transparent" | "keplr"
+): string => {
+ if (!sourceAddress) return "";
+
+ // Use selected token type if available, otherwise fall back to source address format
+ if (selectedTokenType) {
+ switch (selectedTokenType) {
+ case "shielded":
+ return namadaShieldedIcon;
+ case "transparent":
+ return namadaTransparentIcon;
+ case "keplr":
+ return wallets.keplr.iconUrl;
+ default:
+ break;
+ }
+ }
+
+ // Fallback to original logic if token type not specified
+ if (isShieldedAddress(sourceAddress)) {
+ return namadaShieldedIcon;
+ } else if (isTransparentAddress(sourceAddress)) {
+ return namadaTransparentIcon;
+ } else {
+ return wallets.keplr.iconUrl;
+ }
+};
+
export const TransferSource = ({
- chain,
- asset,
isLoadingAssets,
- wallet,
- walletAddress,
- openProviderSelector,
- openChainSelector,
- openAssetSelector,
+ isSubmitting,
+ asset,
+ originalAddress,
availableAmount,
availableAmountMinusFees,
amount,
+ sourceAddress,
+ openAssetSelector,
onChangeAmount,
- isShieldedAddress,
- isSyncingMasp,
- onChangeShielded,
- isSubmitting,
}: TransferSourceProps): JSX.Element => {
+ const selectedTokenType =
+ isTransparentAddress(sourceAddress ?? "") ? "transparent"
+ : isShieldedAddress(sourceAddress ?? "") ? "shielded"
+ : "keplr";
+
return (
-
- {/** Intro header - Ex: "IBC To Namada" */}
- {onChangeShielded && chain?.chain_name === "namada" && !isSubmitting && (
-
- }
- syncedChildren={Shielded sync completed
}
- />
-
- )}
-
- ),
- className:
- isShieldedAddress ? "text-yellow" : (
- clsx("text-yellow/50", {
- "hover:text-yellow/80": !isSyncingMasp,
- })
- ),
- buttonProps: { disabled: isSyncingMasp },
- },
- {
- id: "transparent",
- text: "Transparent",
- className:
- !isShieldedAddress ? "text-white" : (
- "text-white/50 hover:text-white/80"
- ),
- },
- ]}
- onChange={() => onChangeShielded(!isShieldedAddress)}
- />
-
+
-
- {!walletAddress && (
-
- )}
- {walletAddress && wallet && (
-
+ {sourceAddress && (
+
+
+

+
+ {sourceAddress}
+
+
+
)}
-
- {/** Asset selector */}
{!isSubmitting && (
-
-
+
onChangeAmount?.(e.target.value)}
placeholder="Amount"
@@ -156,13 +134,13 @@ export const TransferSource = ({
)}
- {/** Available amount footer */}
{!isSubmitting && asset && availableAmountMinusFees && (