Skip to content
Merged
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
@@ -0,0 +1,29 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import TronUnstakingBanner from './TronUnstakingBanner';
import { TronUnstakingBannerTestIds } from './TronUnstakingBanner.testIds';
import { strings } from '../../../../../../../locales/i18n';

describe('TronUnstakingBanner', () => {
it('renders the unstaking text with the given amount', () => {
const { getByTestId } = render(<TronUnstakingBanner amount="500" />);

const expected = strings('stake.tron.trx_unstaking_in_progress', {
amount: '500',
});
expect(
getByTestId(TronUnstakingBannerTestIds.BANNER_TEXT),
).toHaveTextContent(expected);
});

it('renders with a different amount', () => {
const { getByTestId } = render(<TronUnstakingBanner amount="1,234.5" />);

const expected = strings('stake.tron.trx_unstaking_in_progress', {
amount: '1,234.5',
});
expect(
getByTestId(TronUnstakingBannerTestIds.BANNER_TEXT),
).toHaveTextContent(expected);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum TronUnstakingBannerTestIds {
BANNER_TEXT = 'tron-unstaking-banner',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import { strings } from '../../../../../../../locales/i18n';
import Banner, {
BannerAlertSeverity,
BannerVariant,
} from '../../../../../../component-library/components/Banners/Banner';
import { Text } from '@metamask/design-system-react-native';
import { TronUnstakingBannerTestIds } from './TronUnstakingBanner.testIds';

interface TronUnstakingBannerProps {
amount: string;
}

const TronUnstakingBanner = ({ amount }: TronUnstakingBannerProps) => (
<Banner
severity={BannerAlertSeverity.Info}
variant={BannerVariant.Alert}
description={
<Text testID={TronUnstakingBannerTestIds.BANNER_TEXT}>
{strings('stake.tron.trx_unstaking_in_progress', { amount })}
</Text>
}
/>
);

export default TronUnstakingBanner;
2 changes: 2 additions & 0 deletions app/components/UI/TokenDetails/Views/TokenDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ const TokenDetails: React.FC<{
///: BEGIN:ONLY_INCLUDE_IF(tron)
isTronNative,
stakedTrxAsset,
inLockPeriodBalance,
///: END:ONLY_INCLUDE_IF
} = useTokenBalance(token);

Expand Down Expand Up @@ -210,6 +211,7 @@ const TokenDetails: React.FC<{
///: BEGIN:ONLY_INCLUDE_IF(tron)
isTronNative={isTronNative}
stakedTrxAsset={stakedTrxAsset}
inLockPeriodBalance={inLockPeriodBalance}
///: END:ONLY_INCLUDE_IF
/>
<ActivityHeader
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { isCaipAssetType } from '@metamask/utils';
import { formatAddressToAssetId } from '@metamask/bridge-controller';
///: BEGIN:ONLY_INCLUDE_IF(tron)
import TronEnergyBandwidthDetail from '../../AssetOverview/TronEnergyBandwidthDetail/TronEnergyBandwidthDetail';
import TronUnstakingBanner from '../../Earn/components/Tron/TronUnstakingBanner/TronUnstakingBanner';
///: END:ONLY_INCLUDE_IF
import MarketClosedActionButton from '../../AssetOverview/MarketClosedActionButton';
import { IconName } from '../../../../component-library/components/Icons/Icon';
Expand Down Expand Up @@ -113,6 +114,10 @@ const styleSheet = (params: { theme: Theme }) => {
perpsPositionTitle: {
marginBottom: 8,
} as TextStyle,
bannerWrapper: {
paddingHorizontal: 16,
marginTop: 8,
} as ViewStyle,
});
};

Expand Down Expand Up @@ -156,6 +161,7 @@ export interface AssetOverviewContentProps {
// Tron-specific
isTronNative?: boolean;
stakedTrxAsset?: TokenI;
inLockPeriodBalance?: string;
onMarketInsightsDisplayResolved?: (isDisplayed: boolean) => void;
}

Expand Down Expand Up @@ -193,6 +199,7 @@ const AssetOverviewContent: React.FC<AssetOverviewContentProps> = ({
goToSwaps,
isTronNative,
stakedTrxAsset,
inLockPeriodBalance,
onMarketInsightsDisplayResolved,
}) => {
const { styles } = useStyles(styleSheet, {});
Expand Down Expand Up @@ -524,6 +531,15 @@ const AssetOverviewContent: React.FC<AssetOverviewContentProps> = ({
)
///: END:ONLY_INCLUDE_IF
}
{
///: BEGIN:ONLY_INCLUDE_IF(tron)
isTronNative && inLockPeriodBalance && (
<View style={styles.bannerWrapper}>
<TronUnstakingBanner amount={inLockPeriodBalance} />
</View>
)
///: END:ONLY_INCLUDE_IF
}
{isMarketInsightsEnabled &&
marketInsightsReport &&
marketInsightsCaip19Id ? (
Expand Down
149 changes: 144 additions & 5 deletions app/components/UI/TokenDetails/hooks/useTokenBalance.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Asset } from '@metamask/assets-controllers';
import { Hex } from '@metamask/utils';
import { renderHookWithProvider } from '../../../../util/test/renderWithProvider';
import { useTokenBalance } from './useTokenBalance';
import { TokenI } from '../../Tokens/types';
Expand All @@ -8,6 +10,24 @@ import {
} from '../../../../selectors/assets/assets-list';
import { createStakedTrxAsset } from '../../AssetOverview/utils/createStakedTrxAsset';

const TRON_MAINNET_CHAIN_ID = 'tron:728126428';

const createMockTronAsset = (symbol: string, balance: string): Asset =>
({
accountType: 'tron:eoa',
assetId: `${TRON_MAINNET_CHAIN_ID}/slip44:${symbol}`,
chainId: TRON_MAINNET_CHAIN_ID,
accountId: 'mock-account-id',
image: '',
name: symbol,
symbol,
decimals: 6,
isNative: false,
rawBalance: '0x0' as Hex,
balance,
fiat: { balance: 0, currency: 'usd', conversionRate: 1 },
}) as Asset;

const createEmptySpecialAssetsMap = (): TronSpecialAssetsMap => ({
energy: undefined,
bandwidth: undefined,
Expand Down Expand Up @@ -74,7 +94,6 @@ describe('useTokenBalance', () => {

const { result } = renderHookWithProvider(() => useTokenBalance(token));

// Address is normalized to checksum format for consistent lookup
expect(mockSelectAsset).toHaveBeenCalledWith(expect.any(Object), {
address: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
chainId: token.chainId,
Expand Down Expand Up @@ -111,7 +130,7 @@ describe('useTokenBalance', () => {
it('returns staked TRX asset for Tron native token', () => {
const tronToken = {
address: '',
chainId: 'tron:0x2b6653dc',
chainId: TRON_MAINNET_CHAIN_ID,
ticker: 'TRX',
symbol: 'TRX',
} as TokenI;
Expand All @@ -126,9 +145,9 @@ describe('useTokenBalance', () => {

mockSelectTronResources.mockReturnValue({
...createEmptySpecialAssetsMap(),
stakedTrxForEnergy: { symbol: 'strx-energy', balance: '100' },
stakedTrxForBandwidth: { symbol: 'strx-bandwidth', balance: '200' },
} as TronSpecialAssetsMap);
stakedTrxForEnergy: createMockTronAsset('strx-energy', '100'),
stakedTrxForBandwidth: createMockTronAsset('strx-bandwidth', '200'),
});

mockCreateStakedTrxAsset.mockReturnValue(mockStakedAsset);

Expand All @@ -150,4 +169,124 @@ describe('useTokenBalance', () => {
'200',
);
});

it('returns in-lock-period balance for Tron native token', () => {
const tronToken = {
address: '',
chainId: TRON_MAINNET_CHAIN_ID,
ticker: 'TRX',
symbol: 'TRX',
} as TokenI;

mockSelectAsset.mockReturnValue({
balance: '1000',
balanceFiat: '$100.00',
symbol: 'TRX',
} as TokenI);

mockSelectTronResources.mockReturnValue({
...createEmptySpecialAssetsMap(),
trxInLockPeriod: createMockTronAsset('trx-in-lock-period', '20'),
});

const { result } = renderHookWithProvider(() => useTokenBalance(tronToken));

expect(result.current.inLockPeriodBalance).toBe('20');
});

it('returns undefined for in-lock-period when balance is zero', () => {
const tronToken = {
address: '',
chainId: TRON_MAINNET_CHAIN_ID,
ticker: 'TRX',
symbol: 'TRX',
} as TokenI;

mockSelectAsset.mockReturnValue({
balance: '1000',
balanceFiat: '$100.00',
symbol: 'TRX',
} as TokenI);

mockSelectTronResources.mockReturnValue({
...createEmptySpecialAssetsMap(),
trxInLockPeriod: createMockTronAsset('trx-in-lock-period', '0'),
});

const { result } = renderHookWithProvider(() => useTokenBalance(tronToken));

expect(result.current.inLockPeriodBalance).toBeUndefined();
});

it('returns undefined for in-lock-period when balance is non-numeric', () => {
const tronToken = {
address: '',
chainId: TRON_MAINNET_CHAIN_ID,
ticker: 'TRX',
symbol: 'TRX',
} as TokenI;

mockSelectAsset.mockReturnValue({
balance: '1000',
balanceFiat: '$100.00',
symbol: 'TRX',
} as TokenI);

mockSelectTronResources.mockReturnValue({
...createEmptySpecialAssetsMap(),
trxInLockPeriod: createMockTronAsset(
'trx-in-lock-period',
'not-a-number',
),
});

const { result } = renderHookWithProvider(() => useTokenBalance(tronToken));

expect(result.current.inLockPeriodBalance).toBeUndefined();
});

it('returns undefined for in-lock-period when balance is empty string', () => {
const tronToken = {
address: '',
chainId: TRON_MAINNET_CHAIN_ID,
ticker: 'TRX',
symbol: 'TRX',
} as TokenI;

mockSelectAsset.mockReturnValue({
balance: '1000',
balanceFiat: '$100.00',
symbol: 'TRX',
} as TokenI);

mockSelectTronResources.mockReturnValue({
...createEmptySpecialAssetsMap(),
trxInLockPeriod: createMockTronAsset('trx-in-lock-period', ''),
});

const { result } = renderHookWithProvider(() => useTokenBalance(tronToken));

expect(result.current.inLockPeriodBalance).toBeUndefined();
});

it('returns undefined for in-lock-period when resources are not available', () => {
const tronToken = {
address: '',
chainId: TRON_MAINNET_CHAIN_ID,
ticker: 'TRX',
symbol: 'TRX',
} as TokenI;

mockSelectAsset.mockReturnValue({
balance: '1000',
balanceFiat: '$100.00',
symbol: 'TRX',
} as TokenI);

mockSelectTronResources.mockReturnValue(createEmptySpecialAssetsMap());

const { result } = renderHookWithProvider(() => useTokenBalance(tronToken));

expect(result.current.inLockPeriodBalance).toBeUndefined();
});
});
20 changes: 17 additions & 3 deletions app/components/UI/TokenDetails/hooks/useTokenBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
} from '../../../../selectors/assets/assets-list';
import { toFormattedAddress } from '../../../../util/address';
///: BEGIN:ONLY_INCLUDE_IF(tron)
import I18n from '../../../../../locales/i18n';
import { formatWithThreshold } from '../../../../util/assets';
import { createStakedTrxAsset } from '../../AssetOverview/utils/createStakedTrxAsset';
///: END:ONLY_INCLUDE_IF

Expand All @@ -20,6 +22,7 @@ export interface UseTokenBalanceResult {
///: BEGIN:ONLY_INCLUDE_IF(tron)
isTronNative: boolean;
stakedTrxAsset: TokenI | undefined;
inLockPeriodBalance: string | undefined;
///: END:ONLY_INCLUDE_IF
}

Expand All @@ -33,9 +36,8 @@ export const useTokenBalance = (token: TokenI): UseTokenBalanceResult => {
);

///: BEGIN:ONLY_INCLUDE_IF(tron)
const { stakedTrxForEnergy, stakedTrxForBandwidth } = useSelector(
selectTronSpecialAssetsBySelectedAccountGroup,
);
const { stakedTrxForEnergy, stakedTrxForBandwidth, trxInLockPeriod } =
useSelector(selectTronSpecialAssetsBySelectedAccountGroup);

const isTronNative =
token.ticker === 'TRX' && String(token.chainId).startsWith('tron:');
Expand All @@ -47,6 +49,17 @@ export const useTokenBalance = (token: TokenI): UseTokenBalanceResult => {
stakedTrxForBandwidth?.balance,
)
: undefined;

const parsedInLockPeriod = trxInLockPeriod
? parseFloat(trxInLockPeriod.balance)
: 0;
const inLockPeriodBalance =
isTronNative && parsedInLockPeriod > 0
? formatWithThreshold(parsedInLockPeriod, 0.00001, I18n.locale, {
minimumFractionDigits: 0,
maximumFractionDigits: 5,
}) || undefined
: undefined;
Comment thread
cursor[bot] marked this conversation as resolved.
///: END:ONLY_INCLUDE_IF

const balance = processedAsset?.balance;
Expand All @@ -60,6 +73,7 @@ export const useTokenBalance = (token: TokenI): UseTokenBalanceResult => {
///: BEGIN:ONLY_INCLUDE_IF(tron)
isTronNative,
stakedTrxAsset,
inLockPeriodBalance,
///: END:ONLY_INCLUDE_IF
};
};
Expand Down
4 changes: 4 additions & 0 deletions app/util/assets/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ describe('formatWithThreshold', () => {
);
});

test('returns an empty string when amount is NaN', () => {
expect(formatWithThreshold(NaN, 10, 'en-US', enUSCurrencyOptions)).toBe('');
});

test('formats zero correctly in en-US currency format', () => {
expect(formatWithThreshold(0, 10, 'en-US', enUSCurrencyOptions)).toBe(
'$0.00',
Expand Down
2 changes: 1 addition & 1 deletion app/util/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const formatWithThreshold = (
locale: string,
options: Intl.NumberFormatOptions,
): string => {
if (amount === null) {
if (amount === null || isNaN(amount)) {
return '';
}

Expand Down
3 changes: 2 additions & 1 deletion locales/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -6093,7 +6093,8 @@
"fee": "Fee",
"errors": {
"insufficient_balance": "You don't have enough resource balance to do this action."
}
},
"trx_unstaking_in_progress": "Unstaking {{amount}} TRX in progress. It takes 14 days for unstaking."
},
"stake_eth": "Stake ETH",
"unstake_eth": "Unstake ETH",
Expand Down
Loading