Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [lw-11830]: handle not delegated stake keys scenarios in send flow #1543

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,13 @@

const getWrapper =
() =>
({ children }: { children: React.ReactNode }) =>
(
<AppSettingsProvider>
<StoreProvider appMode={APP_MODE_BROWSER}>
<I18nextProvider i18n={i18n}>{children}</I18nextProvider>
</StoreProvider>
</AppSettingsProvider>
);
({ children }: { children: React.ReactNode }) => (

Check failure on line 73 in apps/browser-extension-wallet/src/hooks/__tests__/useCollateral.test.tsx

View workflow job for this annotation

GitHub Actions / Release package

Insert `⏎···`
<AppSettingsProvider>

Check failure on line 74 in apps/browser-extension-wallet/src/hooks/__tests__/useCollateral.test.tsx

View workflow job for this annotation

GitHub Actions / Release package

Insert `··`
<StoreProvider appMode={APP_MODE_BROWSER}>

Check failure on line 75 in apps/browser-extension-wallet/src/hooks/__tests__/useCollateral.test.tsx

View workflow job for this annotation

GitHub Actions / Release package

Insert `··`
<I18nextProvider i18n={i18n}>{children}</I18nextProvider>

Check failure on line 76 in apps/browser-extension-wallet/src/hooks/__tests__/useCollateral.test.tsx

View workflow job for this annotation

GitHub Actions / Release package

Insert `··`
</StoreProvider>

Check failure on line 77 in apps/browser-extension-wallet/src/hooks/__tests__/useCollateral.test.tsx

View workflow job for this annotation

GitHub Actions / Release package

Insert `··`
</AppSettingsProvider>

Check failure on line 78 in apps/browser-extension-wallet/src/hooks/__tests__/useCollateral.test.tsx

View workflow job for this annotation

GitHub Actions / Release package

Insert `··`
);

Check failure on line 79 in apps/browser-extension-wallet/src/hooks/__tests__/useCollateral.test.tsx

View workflow job for this annotation

GitHub Actions / Release package

Insert `··`

describe('Testing useCollateral hook', () => {
beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,13 @@

const getWrapper =
({ backgroundService }: { backgroundService?: BackgroundServiceAPIProviderProps['value'] }) =>
({ children }: { children: React.ReactNode }) =>
(
<AppSettingsProvider>
<DatabaseProvider>
<BackgroundServiceAPIProvider value={backgroundService}>{children}</BackgroundServiceAPIProvider>
</DatabaseProvider>
</AppSettingsProvider>
);
({ children }: { children: React.ReactNode }) => (

Check failure on line 120 in apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx

View workflow job for this annotation

GitHub Actions / Release package

Insert `⏎···`
<AppSettingsProvider>

Check failure on line 121 in apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx

View workflow job for this annotation

GitHub Actions / Release package

Insert `··`
<DatabaseProvider>

Check failure on line 122 in apps/browser-extension-wallet/src/hooks/__tests__/useWalletManager.test.tsx

View workflow job for this annotation

GitHub Actions / Release package

Insert `··`
<BackgroundServiceAPIProvider value={backgroundService}>{children}</BackgroundServiceAPIProvider>
</DatabaseProvider>
</AppSettingsProvider>
);

const render = () =>
renderHook(() => useWalletManager(), {
Expand Down
6 changes: 4 additions & 2 deletions apps/browser-extension-wallet/src/hooks/useInitializeTx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@ 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<COIN_SELECTION_ERRORS, TranslationKey>([
[COIN_SELECTION_ERRORS.BALANCE_INSUFFICIENT_ERROR, 'general.errors.insufficientBalance'],
[COIN_SELECTION_ERRORS.NOT_FRAGMENTED_ENOUGH_ERROR, 'general.errors.utxoNotFragmentedEnough'],
[COIN_SELECTION_ERRORS.FULLY_DEPLETED_ERROR, 'general.errors.utxoFullyDepleted'],
[COIN_SELECTION_ERRORS.MAXIMUM_INPUT_COUNT_EXCEEDED_ERROR, 'general.errors.maximumInputCountExceeded'],
[COIN_SELECTION_ERRORS.BUNDLE_AMOUNT_IS_EMPTY, 'general.errors.bundleAmountIsEmpty']
[COIN_SELECTION_ERRORS.BUNDLE_AMOUNT_IS_EMPTY, 'general.errors.bundleAmountIsEmpty'],
[COIN_SELECTION_ERRORS.AVAILABLE_BALANCE_INSUFFICIENT_ERROR, 'general.errors.insufficientAvailableBalance']
]);

export const getErrorMessage =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface Props {
coinBalance: string;
isBundle: boolean;
insufficientBalanceInputs: string[];
insufficientAvailableBalanceInputs: string[];
reachedMaxAmountList: Set<string | Wallet.Cardano.AssetId>;
assets: Map<Wallet.Cardano.AssetId, Wallet.Asset.AssetInfo>;
setIsBundle: (value: boolean) => void;
Expand All @@ -31,6 +32,7 @@ export const BundlesList = ({
isBundle,
setIsBundle,
insufficientBalanceInputs,
insufficientAvailableBalanceInputs,
reachedMaxAmountList,
assets,
assetBalances,
Expand Down Expand Up @@ -83,11 +85,13 @@ export const BundlesList = ({
)}
<AddressInput row={bundleId} currentNetwork={currentChain.networkId} isPopupView={isPopupView} />
<CoinInput
isPopupView={isPopupView}
bundleId={bundleId}
assets={assets}
assetBalances={assetBalances}
coinBalance={coinBalance}
insufficientBalanceInputs={insufficientBalanceInputs}
insufficientAvailableBalanceInputs={insufficientAvailableBalanceInputs}
onAddAsset={() => handleAssetPicker(bundleId)}
openAssetPicker={(coinId) => handleAssetPicker(bundleId, coinId)}
canAddMoreAssets={canAddMoreAssets(bundleId)}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { getTemporaryTxDataFromStorage } from '../../../helpers';
import { UseSelectedCoinsProps, useSelectedCoins } from './useSelectedCoins';
import { useRewardAccountsData } from '@src/views/browser-view/features/staking/hooks';
import { LockedStakeRewardsBanner } from '../LockedStakeRewardsBanner/LockedStakeRewardsBanner';

export type CoinInputProps = {
bundleId: string;
assetBalances: Wallet.Cardano.Value['assets'];
canAddMoreAssets?: boolean;
onAddAsset?: () => void;
spendableCoin: bigint;
isPopupView?: boolean;
} & Omit<UseSelectedCoinsProps, 'bundleId' | 'assetBalances'>;

export const CoinInput = ({
Expand All @@ -20,11 +23,13 @@ export const CoinInput = ({
onAddAsset,
canAddMoreAssets,
spendableCoin,
isPopupView,
...selectedCoinsProps
}: CoinInputProps): React.ReactElement => {
const { t } = useTranslation();
const { setCoinValues } = useCoinStateSelector(bundleId);
const { selectedCoins } = useSelectedCoins({ bundleId, assetBalances, spendableCoin, ...selectedCoinsProps });
const { lockedStakeRewards } = useRewardAccountsData();

useEffect(() => {
const { tempOutputs } = getTemporaryTxDataFromStorage();
Expand All @@ -33,11 +38,15 @@ export const CoinInput = ({
}, [bundleId, setCoinValues]);

return (
<AssetInputList
disabled={(!!assetBalances && assetBalances?.size === 0) || !canAddMoreAssets}
rows={selectedCoins}
onAddAsset={onAddAsset}
translations={{ addAsset: t('browserView.transaction.send.advanced.asset') }}
/>
<>
<AssetInputList
disabled={(!!assetBalances && assetBalances?.size === 0) || !canAddMoreAssets}
rows={selectedCoins}
onAddAsset={onAddAsset}
translations={{ addAsset: t('browserView.transaction.send.advanced.asset') }}
isPopupView={isPopupView}
/>
{!!lockedStakeRewards && <LockedStakeRewardsBanner isPopupView={isPopupView} />}
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-magic-numbers */
const mockCoinStateSelector = {
uiOutputs: [],
Expand All @@ -10,6 +11,7 @@ const mockUseCurrencyStore = jest.fn().mockReturnValue({ fiatCurrency: { code: '
const mockUseWalletStore = jest.fn().mockReturnValue({
walletUI: { cardanoCoin: { id: '1', name: 'Cardano', decimals: 6, symbol: 'ADA' }, appMode: 'popup' }
});
const mockUseRewardAccountsData = jest.fn().mockReturnValue({ lockedStakeRewards: 0 });
const mockUseCoinStateSelector = jest.fn().mockReturnValue(mockCoinStateSelector);
const mockUseBuiltTxState = jest.fn().mockReturnValue({ builtTxData: { error: undefined } });
const mockUseAddressState = jest.fn().mockReturnValue({ address: undefined });
Expand Down Expand Up @@ -41,6 +43,12 @@ jest.mock('@stores', (): typeof Stores => ({
...jest.requireActual<typeof Stores>('@stores'),
useWalletStore: mockUseWalletStore
}));

jest.mock('@src/views/browser-view/features/staking/hooks', () => ({
...jest.requireActual<any>('@src/views/browser-view/features/staking/hooks'),
useRewardAccountsData: mockUseRewardAccountsData
}));

jest.mock('../../../../store', (): typeof SendTransactionStore => ({
...jest.requireActual<typeof SendTransactionStore>('../../../../store'),
useCoinStateSelector: mockUseCoinStateSelector,
Expand Down Expand Up @@ -191,6 +199,33 @@ describe('useSelectedCoin', () => {
expect(result.current.selectedCoins).toHaveLength(1);
expect(result.current.selectedCoins[0].coin).toEqual({ id: '1', ticker: 'ADA', balance: 'Balance: 1.00M' });
});
test('gets coin properties from walletUI cardanoCoin in store with compacts coin balance and locked rewards', () => {
mockUseRewardAccountsData.mockReturnValueOnce({
lockedStakeRewards: '10000000000'
});

mockUseCoinStateSelector.mockReturnValueOnce({
...mockCoinStateSelector,
uiOutputs: [{ id: '1', value: '100' }]
});
const props: UseSelectedCoinsProps = {
assetBalances: new Map(),
assets: new Map(),
bundleId: 'bundleId',
coinBalance: '1010000000000',
spendableCoin: BigInt(100)
};
const { result } = renderUseSelectedCoins(props);

expect(result.current.selectedCoins).toHaveLength(1);
expect(result.current.selectedCoins[0].coin).toEqual({
id: '1',
ticker: 'ADA',
balance: 'Balance: 1.01M',
availableBalance: 'Available Balance: 1.00M',
lockedStakeRewards: 'Locked Stake Rewards: 10,000.00'
});
});

test('converts coin value to fiat and set decimals from walletUI cardanoCoin', () => {
mockUseCoinStateSelector.mockReturnValueOnce({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,48 +30,63 @@ 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,
hasReachedMaxAmount: false,
allowFloat: true
allowFloat: true,
hasReachedMaxAvailableAmount: true,
lockedStakeRewards: '0.00',
totalADA: '0.00'
});
});
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,
hasReachedMaxAmount: true,
allowFloat: true
allowFloat: true,
hasReachedMaxAvailableAmount: false,
lockedStakeRewards: '0.00',
totalADA: '1.00'
});
});
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,
hasReachedMaxAmount: false,
allowFloat: true
allowFloat: true,
hasReachedMaxAvailableAmount: false,
lockedStakeRewards: '0.00',
totalADA: '20.00'
});
});
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,
hasReachedMaxAmount: false,
allowFloat: true
allowFloat: true,
hasReachedMaxAvailableAmount: false,
lockedStakeRewards: '0.00',
totalADA: '20.00'
});
});
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,
hasReachedMaxAmount: true,
allowFloat: true
allowFloat: true,
hasReachedMaxAvailableAmount: false,
lockedStakeRewards: '0.00',
totalADA: '20.00'
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,6 +39,7 @@ export interface UseSelectedCoinsProps {
/** Coin balance (ADA) in lovelace */
coinBalance: string;
insufficientBalanceInputs?: Array<string>;
insufficientAvailableBalanceInputs?: Array<string>;
openAssetPicker?: (id: string) => void;
spendableCoin: bigint;
}
Expand All @@ -54,6 +56,7 @@ export const useSelectedCoins = ({
assets,
coinBalance,
insufficientBalanceInputs,
insufficientAvailableBalanceInputs,
openAssetPicker,
bundleId,
spendableCoin
Expand All @@ -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
*/
Expand All @@ -88,14 +93,18 @@ 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;
}
// eslint-disable-next-line consistent-return
return undefined;
},
[address, assetInputList, builtTxError, insufficientBalanceInputs]
[address, assetInputList, builtTxError, insufficientBalanceInputs, insufficientAvailableBalanceInputs]
);

const handleOnChangeCoin = useCallback(
Expand Down Expand Up @@ -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',
Expand All @@ -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) }),
...(rewardAcountsData.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}`,
Expand Down
Loading
Loading