diff --git a/apps/browser-extension-wallet/src/hooks/useInitializeTx.ts b/apps/browser-extension-wallet/src/hooks/useInitializeTx.ts index a4b3dfbd5..72269dd4a 100644 --- a/apps/browser-extension-wallet/src/hooks/useInitializeTx.ts +++ b/apps/browser-extension-wallet/src/hooks/useInitializeTx.ts @@ -16,7 +16,8 @@ export enum COIN_SELECTION_ERRORS { NOT_FRAGMENTED_ENOUGH_ERROR = 'UTxO Not Fragmented Enough', FULLY_DEPLETED_ERROR = 'UTxO Fully Depleted', MAXIMUM_INPUT_COUNT_EXCEEDED_ERROR = 'Maximum Input Count Exceeded', - BUNDLE_AMOUNT_IS_EMPTY = 'Bundle amount is empty' + BUNDLE_AMOUNT_IS_EMPTY = 'Bundle amount is empty', + AVAILABLE_BALANCE_INSUFFICIENT_ERROR = 'Amount is less than the total balance, but more than the available balance' } export const coinSelectionErrors = new Map([ @@ -24,7 +25,8 @@ export const coinSelectionErrors = new Map; assets: Map; setIsBundle: (value: boolean) => void; @@ -31,6 +32,7 @@ export const BundlesList = ({ isBundle, setIsBundle, insufficientBalanceInputs, + insufficientAvailableBalanceInputs, reachedMaxAmountList, assets, assetBalances, @@ -83,11 +85,13 @@ export const BundlesList = ({ )} handleAssetPicker(bundleId)} openAssetPicker={(coinId) => handleAssetPicker(bundleId, coinId)} canAddMoreAssets={canAddMoreAssets(bundleId)} diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/CoinInput.module.scss b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/CoinInput.module.scss index aadcb1f42..2ac2917f3 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/CoinInput.module.scss +++ b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/CoinInput.module.scss @@ -1,15 +1,18 @@ -@import '../../../../../../../../../packages/common/src/ui/styles/theme.scss'; +@import '../../../../../../../../../../packages/common/src/ui/styles/theme.scss'; -.errorParagraph { - color: var(--data-red); - font-size: var(--bodyXS); - font-style: normal; - font-weight: 400; - line-height: size_unit(2); - letter-spacing: 0.02em; - text-align: left; +.banner { + margin-bottom: 0px !important; + margin-top: 0px !important; + &.popupView { + margin-bottom: 0; + } + + .bannerDescription { + max-width: 315px; + } } -.coinInputContainer { - flex: 1; +.bannerPopup { + border-radius: 16px; + background-color: var(--dark-mode-dark-grey-plus, var(--color-magnolia, #fcf5e3)); } diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/CoinInput.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/CoinInput.tsx index 62463e93c..75ceab900 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/CoinInput.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/CoinInput.tsx @@ -1,10 +1,18 @@ import { AssetInputList } from '@lace/core'; +import cn from 'classnames'; +import { useHistory } from 'react-router-dom'; import { useCoinStateSelector } from '../../../store'; import { Wallet } from '@lace/cardano'; import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { getTemporaryTxDataFromStorage } from '../../../helpers'; import { UseSelectedCoinsProps, useSelectedCoins } from './useSelectedCoins'; +import { Banner } from '@lace/common'; +import { walletRoutePaths } from '@routes'; +import styles from './CoinInput.module.scss'; +import { Box, Button, Flex, Text } from '@input-output-hk/lace-ui-toolkit'; +import { useDrawer } from '@src/views/browser-view/stores'; +import ExclamationCircleOutline from '@src/assets/icons/red-exclamation-circle.component.svg'; export type CoinInputProps = { bundleId: string; @@ -12,6 +20,7 @@ export type CoinInputProps = { canAddMoreAssets?: boolean; onAddAsset?: () => void; spendableCoin: bigint; + isPopupView?: boolean; } & Omit; export const CoinInput = ({ @@ -20,11 +29,14 @@ export const CoinInput = ({ onAddAsset, canAddMoreAssets, spendableCoin, + isPopupView, ...selectedCoinsProps }: CoinInputProps): React.ReactElement => { const { t } = useTranslation(); + const history = useHistory(); const { setCoinValues } = useCoinStateSelector(bundleId); const { selectedCoins } = useSelectedCoins({ bundleId, assetBalances, spendableCoin, ...selectedCoinsProps }); + const [, setIsDrawerVisible] = useDrawer(); useEffect(() => { const { tempOutputs } = getTemporaryTxDataFromStorage(); @@ -32,12 +44,42 @@ export const CoinInput = ({ setCoinValues(bundleId, tempOutputs); }, [bundleId, setCoinValues]); + const onGoToStaking = () => { + const path = isPopupView ? walletRoutePaths.earn : walletRoutePaths.staking; + setIsDrawerVisible(); + history.push(path); + }; + return ( - + <> + + {isPopupView ? ( + + {t('general.errors.lockedStakeRewards.description')} + + + ) : ( + } + popupView={isPopupView} + className={cn(styles.banner, { [styles.popupView]: isPopupView })} + message={{t('general.errors.lockedStakeRewards.description')}} + buttonMessage={t('general.errors.lockedStakeRewards.cta')} + onButtonClick={onGoToStaking} + withIcon + /> + )} + ); }; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/__tests__/util.test.ts b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/__tests__/util.test.ts index 64c5e159b..5f5266178 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/__tests__/util.test.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/__tests__/util.test.ts @@ -30,7 +30,7 @@ describe('CoinInput util', () => { describe('getADACoinProperties', () => { test('returns 0 for availableADA and false for hasMaxBtn when balance is 0', () => { - expect(getADACoinProperties('0', '1000000', '0', '0')).toEqual({ + expect(getADACoinProperties('0', '1000000', '0', '0', '0')).toEqual({ availableADA: '0.00', max: '1', hasMaxBtn: false, @@ -39,7 +39,7 @@ describe('CoinInput util', () => { }); }); test('returns 0 for max and true for hasReachedMaxAmount when spendable coins is 0', () => { - expect(getADACoinProperties('1000000', '0', '0', '0')).toEqual({ + expect(getADACoinProperties('1000000', '0', '0', '0', '0')).toEqual({ availableADA: '1.00', max: '0', hasMaxBtn: true, @@ -48,7 +48,7 @@ describe('CoinInput util', () => { }); }); test('returns formatted balance as availableADA, and the spendable coin in ADA as max when there is no spending', () => { - expect(getADACoinProperties('20000000', '10000000', '0', '0')).toEqual({ + expect(getADACoinProperties('20000000', '10000000', '0', '0', '0')).toEqual({ availableADA: '20.00', max: '10', hasMaxBtn: true, @@ -57,7 +57,7 @@ describe('CoinInput util', () => { }); }); test('returns the calculated max amount when there is less spent coin than spendable coin', () => { - expect(getADACoinProperties('20000000', '10000000', '5', '2')).toEqual({ + expect(getADACoinProperties('20000000', '10000000', '5', '2', '0')).toEqual({ availableADA: '20.00', max: '7', hasMaxBtn: true, @@ -66,7 +66,7 @@ describe('CoinInput util', () => { }); }); test('returns max amount as 0 and hasReachedMaxAmount as true when there is more spent coin than spendable coin', () => { - expect(getADACoinProperties('20000000', '10000000', '10', '0')).toEqual({ + expect(getADACoinProperties('20000000', '10000000', '10', '0', '0')).toEqual({ availableADA: '20.00', max: '0', hasMaxBtn: true, diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/useSelectedCoins.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/useSelectedCoins.tsx index 501aa3f81..facd4a640 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/useSelectedCoins.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/useSelectedCoins.tsx @@ -24,6 +24,7 @@ import { APP_MODE_POPUP } from '@src/utils/constants'; import { useCallback } from 'react'; import { AssetInfo } from '../../../types'; import { MAX_NFT_TICKER_LENGTH, MAX_TOKEN_TICKER_LENGTH } from '../../../constants'; +import { useRewardAccountsData } from '@src/views/browser-view/features/staking/hooks'; interface InputFieldActionParams { id: string; @@ -38,6 +39,7 @@ export interface UseSelectedCoinsProps { /** Coin balance (ADA) in lovelace */ coinBalance: string; insufficientBalanceInputs?: Array; + insufficientAvailableBalanceInputs?: Array; openAssetPicker?: (id: string) => void; spendableCoin: bigint; } @@ -54,6 +56,7 @@ export const useSelectedCoins = ({ assets, coinBalance, insufficientBalanceInputs, + insufficientAvailableBalanceInputs, openAssetPicker, bundleId, spendableCoin @@ -76,6 +79,8 @@ export const useSelectedCoins = ({ // This is used to focus on the last asset after the changes in the bundle const [bundleIdOfLastAddedCoin] = useCurrentRow(); + const rewardAcountsData = useRewardAccountsData(); + /** * Displays the error with highest priority. Tx level error > Asset level error > Bundle level error */ @@ -88,6 +93,10 @@ export const useSelectedCoins = ({ if (assetInput?.value && assetInput.value !== '0' && !!insufficientBalanceInputs?.includes(inputId)) { return COIN_SELECTION_ERRORS.BALANCE_INSUFFICIENT_ERROR; } + // If the asset has an input value but it is less than the total balance, but more than the available balance, then display an insufficient available balance error. + if (assetInput?.value && assetInput.value !== '0' && !!insufficientAvailableBalanceInputs?.includes(inputId)) { + return COIN_SELECTION_ERRORS.AVAILABLE_BALANCE_INSUFFICIENT_ERROR; + } // If there is a valid address but all coins have 0 as value or it's missing, then display a bundle empty error if (address && isValidAddress(address) && assetInputList.every((item) => !(item.value && Number(item.value)))) { return COIN_SELECTION_ERRORS.BUNDLE_AMOUNT_IS_EMPTY; @@ -95,7 +104,7 @@ export const useSelectedCoins = ({ // eslint-disable-next-line consistent-return return undefined; }, - [address, assetInputList, builtTxError, insufficientBalanceInputs] + [address, assetInputList, builtTxError, insufficientBalanceInputs, insufficientAvailableBalanceInputs] ); const handleOnChangeCoin = useCallback( @@ -172,11 +181,19 @@ export const useSelectedCoins = ({ // Asset is cardano coin if (assetInputItem.id === cardanoCoin.id) { - const { availableADA, hasReachedMaxAmount, ...adaCoinProps } = getADACoinProperties( + const { + totalADA, + availableADA, + lockedStakeRewards, + hasReachedMaxAmount, + hasReachedMaxAvailableAmount, + ...adaCoinProps + } = getADACoinProperties( coinBalance, spendableCoin?.toString(), tokensUsed[cardanoCoin.id] || '0', - assetInputItem?.value || '0' + assetInputItem?.value || '0', + rewardAcountsData.lockedStakeRewards.toString() || '0' ); const fiatValue = Wallet.util.convertAdaToFiat({ ada: assetInputItem?.value || '0', @@ -186,10 +203,20 @@ export const useSelectedCoins = ({ ...commonCoinProps, ...adaCoinProps, hasReachedMaxAmount: hasReachedMaxAmount && error !== COIN_SELECTION_ERRORS.FULLY_DEPLETED_ERROR, + hasReachedMaxAvailableAmount: + hasReachedMaxAvailableAmount && error !== COIN_SELECTION_ERRORS.FULLY_DEPLETED_ERROR, coin: { id: cardanoCoin.id, ticker: cardanoCoin.symbol, - balance: t('send.balanceAmount', { amount: compactNumberWithUnit(availableADA) }) + balance: t('send.balanceAmount', { amount: compactNumberWithUnit(totalADA) }), + ...(lockedStakeRewards && { + availableBalance: t('send.availableBalanceAmount', { + amount: compactNumberWithUnit(availableADA) + }), + lockedStakeRewards: t('send.lockedStakeRewardsAmount', { + amount: compactNumberWithUnit(lockedStakeRewards.toString()) + }) + }) }, formattedFiatValue: `= ${compactNumberWithUnit(fiatValue)} ${fiatCurrency?.code}`, fiatValue: `= ${fiatValue} ${fiatCurrency?.code}`, diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/util.ts b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/util.ts index 563a7d363..e05894aeb 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/util.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/CoinInput/util.ts @@ -28,7 +28,10 @@ export const getMaxSpendableAmount = (totalSpendableBalance = '0', totalSpent = }; type ADARow = { + totalADA: string; availableADA: string; + lockedStakeRewards: string; + hasReachedMaxAvailableAmount: boolean; } & Pick; /** @@ -38,24 +41,30 @@ type ADARow = { * @param spendableCoin The amount of coins that can be spent in lovelaces. * @param spentCoins The total amount of coins spent in ADA (including current input). * @param currentSpendingAmount The amount entered in the current input in ADA. + * @param lockedStakeRewards Locked Stake Rewards that cannot be withdrawn. */ export const getADACoinProperties = ( balance: string, spendableCoin: string, spentCoins: string, - currentSpendingAmount: string + currentSpendingAmount: string, + lockedStakeRewards: string ): ADARow => { // Convert to ADA - const availableADA = Wallet.util.lovelacesToAdaString(balance); + const totalADA = Wallet.util.lovelacesToAdaString(balance); + const availableADA = Wallet.util.lovelacesToAdaString(new BigNumber(balance).minus(lockedStakeRewards).toString()); const spendableCoinInAda = Wallet.util.lovelacesToAdaString(spendableCoin, undefined, BigNumber.ROUND_DOWN); // Calculate max amount in ADA const max = getMaxSpendableAmount(spendableCoinInAda, spentCoins, currentSpendingAmount); return { + totalADA, availableADA, + lockedStakeRewards: Wallet.util.lovelacesToAdaString(lockedStakeRewards.toString()), max, allowFloat: true, - hasMaxBtn: Number(availableADA) > 0, - hasReachedMaxAmount: new BigNumber(spentCoins).gte(spendableCoinInAda) + hasMaxBtn: Number(totalADA) > 0, + hasReachedMaxAmount: new BigNumber(spentCoins).gte(spendableCoinInAda), + hasReachedMaxAvailableAmount: lockedStakeRewards && new BigNumber(spentCoins).gte(availableADA) }; }; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/Form.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/Form.tsx index 23efa5a98..2165feef9 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/Form.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/Form.tsx @@ -12,12 +12,13 @@ import { useMaxAda } from '@hooks/useMaxAda'; import BundleIcon from '../../../../../../assets/icons/bundle-icon.component.svg'; import { getFee } from '../SendTransactionSummary'; import { useBuiltTxState, useSpentBalances, useLastFocusedInput, useOutputs } from '../../store'; -import { getReachedMaxAmountList } from '../../helpers'; +import { getNextInsuffisientBalanceInputs, getReachedMaxAmountList, hasReachedMaxAmountAda } from '../../helpers'; import { MetadataInput } from './MetadataInput'; import { BundlesList } from './BundlesList'; import { formatAdaAllocation, getNextBundleCoinId } from './util'; import { Box, Text, WarningIconCircleComponent } from '@input-output-hk/lace-ui-toolkit'; import styles from './Form.module.scss'; +import { useRewardAccountsData } from '../../../staking/hooks'; export interface Props { assets: Map; @@ -48,9 +49,11 @@ export const Form = ({ const tokensUsed = useSpentBalances(); const spendableCoin = useMaxAda(); const [insufficientBalanceInputs, setInsufficientBalanceInputs] = useState([]); // we save all the element input ids with insufficient balance error + const [insufficientAvailableBalanceInputs, setInsufficientAvailableBalanceInputs] = useState([]); const { lastFocusedInput } = useLastFocusedInput(); const { fiatCurrency } = useCurrencyStore(); const availableRewards = useObservable(inMemoryWallet?.balance?.rewardAccounts?.rewards$); + const { lockedStakeRewards } = useRewardAccountsData(); const { setNewOutput } = useOutputs(); @@ -75,25 +78,29 @@ export const Form = ({ [assets, availableRewards, balance, cardanoCoin, tokensUsed] ); + const reachedMaxAvailableAmount = + balance?.coins && + hasReachedMaxAmountAda({ + tokensUsed, + balance: BigInt(balance?.coins.toString()) - BigInt(lockedStakeRewards.toString()), + cardanoCoin, + availableRewards + }); + useEffect(() => { if (lastFocusedInput) { const id = lastFocusedInput.split('.')[1]; // we get the coin id (cardano id or asset id) to check if it is in the reachedMaxAmountList - setInsufficientBalanceInputs((prevInputsList) => { - const isInMaxAmountList = reachedMaxAmountList.has(id); // check the if the id exists in reachedMaxAmountList - const isInInsufficientBalanceList = prevInputsList.includes(lastFocusedInput); // check the if input element id exists in insufficient balance list - - // check if the last focused element has insufficient balance and doesn't exists in insufficient balance list - if (isInMaxAmountList && !isInInsufficientBalanceList) { - return [...prevInputsList, lastFocusedInput]; // add it to the insufficient balance list - // check if the last focused element has balance and exists in insufficient balance list - } else if (!isInMaxAmountList && isInInsufficientBalanceList) { - return prevInputsList.filter((inputId) => inputId.split('.')[1] !== id); // remove all items with same coin id (cardano id or asset id) - } - - return prevInputsList; - }); + setInsufficientBalanceInputs(getNextInsuffisientBalanceInputs(lastFocusedInput, reachedMaxAmountList, id)); + + setInsufficientAvailableBalanceInputs( + getNextInsuffisientBalanceInputs( + lastFocusedInput, + new Set(reachedMaxAvailableAmount ? [cardanoCoin.id] : []), + id + ) + ); } - }, [reachedMaxAmountList, lastFocusedInput]); + }, [reachedMaxAmountList, lastFocusedInput, cardanoCoin.id, reachedMaxAvailableAmount]); const fee = uiTx?.fee?.toString() ?? '0'; const totalCost = getFee(fee.toString(), prices?.cardano?.price, cardanoCoin, fiatCurrency); @@ -133,6 +140,7 @@ export const Form = ({ isBundle={isBundle} setIsBundle={setIsBundle} insufficientBalanceInputs={insufficientBalanceInputs} + insufficientAvailableBalanceInputs={insufficientAvailableBalanceInputs} reachedMaxAmountList={reachedMaxAmountList} assets={assets} assetBalances={assetBalances} diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/helpers.ts b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/helpers.ts index 6a1889ad8..04001c0f4 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/helpers.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/helpers.ts @@ -61,6 +61,42 @@ export const getOutputValues = (assets: Array, cardanoCoin: Wallet.Co }; }; +export const getNextInsuffisientBalanceInputs = + (lastFocusedInput: string, reachedMaxAmountList: Set, id: string) => + (prevInputsList: string[]): string[] => { + const isInMaxAmountList = reachedMaxAmountList.has(id); // check the if the id exists in reachedMaxAmountList + const isInInsufficientBalanceList = prevInputsList.includes(lastFocusedInput); // check the if input element id exists in insufficient balance list + + // check if the last focused element has insufficient balance and doesn't exists in insufficient balance list + if (isInMaxAmountList && !isInInsufficientBalanceList) { + return [...prevInputsList, lastFocusedInput]; // add it to the insufficient balance list + // check if the last focused element has balance and exists in insufficient balance list + } else if (!isInMaxAmountList && isInInsufficientBalanceList) { + return prevInputsList.filter((inputId) => inputId.split('.')[1] !== id); // remove all items with same coin id (cardano id or asset id) + } + + return prevInputsList; + }; + +export const hasReachedMaxAmountAda = ({ + tokensUsed, + balance, + exceed = false, + cardanoCoin, + availableRewards = BigInt(0) +}: { + tokensUsed: SpentBalances; + balance: Wallet.Cardano.Lovelace; + exceed?: boolean; + cardanoCoin: Wallet.CoinId; + availableRewards?: bigint; +}): boolean => + tokensUsed[cardanoCoin.id] && balance + ? new BigNumber(tokensUsed[cardanoCoin.id])[exceed ? 'gt' : 'gte']( + Wallet.util.lovelacesToAdaString((balance + availableRewards).toString()) + ) + : false; + export const getReachedMaxAmountList = ({ assets = new Map(), tokensUsed, @@ -76,12 +112,12 @@ export const getReachedMaxAmountList = ({ cardanoCoin: Wallet.CoinId; availableRewards?: bigint; }): (string | Wallet.Cardano.AssetId)[] => { - const reachedMaxAmountAda = - tokensUsed[cardanoCoin.id] && balance?.coins - ? new BigNumber(tokensUsed[cardanoCoin.id])[exceed ? 'gt' : 'gte']( - Wallet.util.lovelacesToAdaString((balance.coins + availableRewards).toString()) - ) - : false; + const reachedMaxAmountAda = hasReachedMaxAmountAda({ + tokensUsed, + balance: balance?.coins, + cardanoCoin, + availableRewards + }); const reachedMaxAmountAssets = balance?.assets?.size ? [...balance.assets] diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/hooks.ts b/apps/browser-extension-wallet/src/views/browser-view/features/staking/hooks.ts index fc86c1b7c..59d711886 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/staking/hooks.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/hooks.ts @@ -1,8 +1,10 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useDelegationStore } from '@src/features/delegation/stores'; import { useWalletStore } from '@stores'; import { withSignTxConfirmation } from '@lib/wallet-api-ui'; import { useSecrets } from '@lace/core'; +import { useObservable } from '@lace/common'; +import { Wallet } from '@lace/cardano'; export const useDelegationTransaction = (): { signAndSubmitTransaction: () => Promise } => { const { password, clearSecrets } = useSecrets(); @@ -17,3 +19,55 @@ export const useDelegationTransaction = (): { signAndSubmitTransaction: () => Pr return { signAndSubmitTransaction }; }; + +export const useRewardAccountsData = (): { + accountsWithRegisteredStakeCredsWithoutVotingDelegation: Wallet.Cardano.RewardAccountInfo[]; + accountsWithRegisteredStakeCreds: Wallet.Cardano.RewardAccountInfo[]; + poolIdToRewardAccountMap: Map; + lockedStakeRewards: BigInt; +} => { + const { inMemoryWallet } = useWalletStore(); + const rewardAccounts = useObservable(inMemoryWallet.delegation.rewardAccounts$); + const accountsWithRegisteredStakeCreds = rewardAccounts?.filter( + ({ credentialStatus }) => Wallet.Cardano.StakeCredentialStatus.Registered === credentialStatus + ); + + const accountsWithRegisteredStakeCredsWithoutVotingDelegation = useMemo( + () => accountsWithRegisteredStakeCreds?.filter(({ dRepDelegatee }) => !dRepDelegatee), + [accountsWithRegisteredStakeCreds] + ); + + const lockedStakeRewards = useMemo( + () => + BigInt( + accountsWithRegisteredStakeCredsWithoutVotingDelegation + ? Wallet.BigIntMath.sum( + accountsWithRegisteredStakeCredsWithoutVotingDelegation.map(({ rewardBalance }) => rewardBalance) + ) + : 0 + ), + [accountsWithRegisteredStakeCredsWithoutVotingDelegation] + ); + + const poolIdToRewardAccountMap = useMemo( + () => + new Map( + accountsWithRegisteredStakeCreds + ?.map((rewardAccount): [string, Wallet.Cardano.RewardAccountInfo] => { + const { delegatee } = rewardAccount; + const delagationInfo = delegatee?.nextNextEpoch || delegatee?.nextEpoch || delegatee?.currentEpoch; + + return [delagationInfo?.id.toString(), rewardAccount]; + }) + .filter(([poolId]) => !!poolId) + ), + [accountsWithRegisteredStakeCreds] + ); + + return { + accountsWithRegisteredStakeCreds, + accountsWithRegisteredStakeCredsWithoutVotingDelegation, + lockedStakeRewards, + poolIdToRewardAccountMap + }; +}; diff --git a/packages/core/src/ui/components/AssetInput/AssetInput.module.scss b/packages/core/src/ui/components/AssetInput/AssetInput.module.scss index 6a89f36cc..d59d56f34 100644 --- a/packages/core/src/ui/components/AssetInput/AssetInput.module.scss +++ b/packages/core/src/ui/components/AssetInput/AssetInput.module.scss @@ -5,12 +5,35 @@ .assetInputContainer { display: flex; flex-direction: column; - justify-content: space-between; + gap: 5px; max-height: 66px; height: 66px; @media (max-width: $breakpoint-popup) { max-height: 60px; + gap: 0px; + } + + &.withLockedRewards { + gap: 10px; + max-height: none; + height: auto; + + @media (max-width: $breakpoint-popup) { + gap: 4px; + max-height: none; + } + .balanceText { + &.fiatValue { + max-height: size_unit(3); + } + } + + .invalidInput { + @media (max-width: 365px) { + margin-bottom: size_unit(2); + } + } } } @@ -110,7 +133,6 @@ } .invalidInput { - margin-top: size_unit(1); color: var(--data-pink); font-size: var(--bodyXS); font-weight: 500; diff --git a/packages/core/src/ui/components/AssetInput/AssetInput.tsx b/packages/core/src/ui/components/AssetInput/AssetInput.tsx index 773f09ca1..969893e80 100644 --- a/packages/core/src/ui/components/AssetInput/AssetInput.tsx +++ b/packages/core/src/ui/components/AssetInput/AssetInput.tsx @@ -10,6 +10,7 @@ import { sanitizeNumber } from '@ui/utils/sanitize-number'; import { useTranslation } from 'react-i18next'; import cn from 'classnames'; import { CoreTranslationKey } from '@lace/translation'; +import { Flex, Text } from '@input-output-hk/lace-ui-toolkit'; const isSameNumberFormat = (num1: string, num2: string) => { if (!num1 || !num2) return false; @@ -21,7 +22,15 @@ const defaultInputWidth = 18; export interface AssetInputProps { inputId: string; - coin: { id: string; balance: string; src?: string; ticker?: string; shortTicker?: string }; + coin: { + id: string; + balance: string; + availableBalance?: string; + lockedStakeRewards?: string; + src?: string; + ticker?: string; + shortTicker?: string; + }; onChange: (args: { value: string; prevValue?: string; id: string; element?: any; maxDecimals?: number }) => void; onBlur?: (args: { value: string; id: string; maxDecimals?: number }) => void; onFocus?: (args: { value: string; id: string; maxDecimals?: number }) => void; @@ -39,11 +48,13 @@ export interface AssetInputProps { hasMaxBtn?: boolean; displayMaxBtn?: boolean; hasReachedMaxAmount?: boolean; + hasReachedMaxAvailableAmount?: boolean; focused?: boolean; onBlurErrors?: Set; getErrorMessage: (message: string) => CoreTranslationKey; setFocusInput?: (input?: string) => void; setFocus?: (focus: boolean) => void; + isPopupView?: boolean; } const placeholderValue = '0'; @@ -71,9 +82,11 @@ export const AssetInput = ({ hasMaxBtn = true, displayMaxBtn = false, hasReachedMaxAmount, + hasReachedMaxAvailableAmount, focused, setFocusInput, - setFocus + setFocus, + isPopupView }: AssetInputProps): React.ReactElement => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const inputRef = useRef(null); @@ -179,8 +192,19 @@ export const AssetInput = ({ const onBlurError = isTouched ? getErrorMessage(error) : undefined; const errorMessage = onBlurErrors?.has(error) ? onBlurError : getErrorMessage(error); + const lockedRewards = ( + <> + {coin.availableBalance} + {coin.lockedStakeRewards} + + ); + return ( -
+
@@ -199,7 +223,7 @@ export const AssetInput = ({ size="small" color="secondary" className={cn(styles.maxBtn, { [styles.show]: displayMaxBtn })} - disabled={hasReachedMaxAmount} + disabled={hasReachedMaxAmount || hasReachedMaxAvailableAmount} > {t('core.assetInput.maxButton')} @@ -226,22 +250,34 @@ export const AssetInput = ({
-

- {coin.balance} -

-
-

+ + {coin.balance} + {!isPopupView && !!coin.lockedStakeRewards && lockedRewards} + + +

{focused ? fiatValue : formattedFiatValue}

-
-
-
- {isInvalid && errorMessage && ( - - {t(errorMessage)} - - )} + {isInvalid && errorMessage && ( + + {t(errorMessage)} + + )} +
+ {isPopupView && !!coin.lockedStakeRewards && ( +
+ + {lockedRewards} + +
+ )}
); }; diff --git a/packages/core/src/ui/components/AssetInput/AssetInputList.tsx b/packages/core/src/ui/components/AssetInput/AssetInputList.tsx index 2a920b17f..db3565da3 100644 --- a/packages/core/src/ui/components/AssetInput/AssetInputList.tsx +++ b/packages/core/src/ui/components/AssetInput/AssetInputList.tsx @@ -17,17 +17,25 @@ export interface AssetInputListProps { onAddAsset?: () => void; disabled?: boolean; translations: TranslationsFor<'addAsset'>; + isPopupView?: boolean; } export const AssetInputList = ({ rows, onAddAsset, disabled, - translations + translations, + isPopupView }: AssetInputListProps): React.ReactElement => (
{rows.map((row, idx) => ( - + ))}