Skip to content

Commit 597b803

Browse files
authored
fix: better check for tron native asset cp-8.3.0 (#33206)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> <!-- mms-check directive vocabulary — read by .github/scripts/shared/pr-template-checks.ts at module load to build the validation plan. Directives are invisible in rendered markdown and must NOT be removed or edited without updating the validator registry. type=text Section must contain non-placeholder prose. type=changelog Section must have a valid CHANGELOG entry: line. type=issue-link Section must have a Fixes:/Closes:/Refs: line with a value. type=manual-testing Section must have real testing steps or an explicit N/A. type=screenshot Section must have evidence (image/URL) or an explicit N/A. type=checklist Section must have all checkboxes consciously checked. required=true|false Whether a missing/invalid section runs the validator at all. blocking=true|false Whether a failure of this check fails the CI workflow. Default: false — failures are shown as warnings in the sticky comment but do not block the PR. Sections without a directive are checked for structural presence only. --> ## **Description** <!-- mms-check: type=text required=true --> <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> Tron native asset is determined via assetId instead of just properties to correct a bug. ## **Changelog** <!-- mms-check: type=changelog required=true blocking=true --> <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: Fixed a bug in which several empty Tron entries appeared in the Earn list ## **Related issues** <!-- mms-check: type=issue-link required=true --> Fixes: https://consensyssoftware.atlassian.net/browse/ASSETS-3428 Fixes: https://consensyssoftware.atlassian.net/browse/MUSD-1162 Fixes: #32110 ## **Manual testing steps** <!-- mms-check: type=manual-testing required=true --> ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- mms-check: type=screenshot required=true --> <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> https://github.com/user-attachments/assets/a5d0e4d6-4d21-4d71-864e-ace9d08570b9 ### **After** <!-- [screenshots/recordings] --> https://github.com/user-attachments/assets/9aacbbc8-46ec-4b6b-b6f6-2810dda87dfd ## **Pre-merge author checklist** <!-- mms-check: type=checklist required=true --> <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [X] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I've included tests if applicable - [X] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Scoped change to earn selector logic with new unit tests; no auth, payments, or vault handling. > > **Overview** > Fixes spurious Tron rows in the Earn list by tightening which token counts as native TRX for pooled staking. > > **`selectEarnTokens`** no longer treats every Tron-chain native token as stakeable TRX. **`isTronNative`** now requires the canonical asset id `tron:728126428/slip44:195` instead of `isNative` + Tron chain id, so related slip44 assets (e.g. lock-period variants) are not surfaced as empty earn entries. > > Tests mock unified account tokens and assert the canonical TRX entry is included with **pooled staking** when TRX staking is enabled, and that non-matching slip44 addresses are excluded. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit b5d9c0d. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent a1e8b19 commit 597b803

2 files changed

Lines changed: 131 additions & 2 deletions

File tree

app/selectors/earnController/earn/index.test.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,20 @@ jest.mock('../../../selectors/multichainAccounts/accounts', () => ({
6565
.internalAccount2,
6666
}));
6767

68+
const TRX_NATIVE_TOKEN_ADDRESS = 'tron:728126428/slip44:195';
69+
const TRON_MAINNET_CHAIN_ID = 'tron:728126428';
70+
71+
const mockSelectAccountTokensAcrossChainsUnified = jest.fn();
72+
73+
jest.mock('../../../selectors/multichain', () => {
74+
const actual = jest.requireActual('../../../selectors/multichain');
75+
return {
76+
...actual,
77+
selectAccountTokensAcrossChainsUnified: (state: RootState) =>
78+
mockSelectAccountTokensAcrossChainsUnified(state),
79+
};
80+
});
81+
6882
const MOCK_ROOT_STATE_WITH_EARN_CONTROLLER = mockEarnControllerRootState();
6983
const MOCK_RATE = {
7084
price: 0.99,
@@ -382,6 +396,13 @@ describe('Earn Controller Selectors', () => {
382396
beforeEach(() => {
383397
jest.resetAllMocks();
384398

399+
const { selectAccountTokensAcrossChainsUnified } = jest.requireActual<
400+
typeof import('../../../selectors/multichain')
401+
>('../../../selectors/multichain');
402+
mockSelectAccountTokensAcrossChainsUnified.mockImplementation(
403+
selectAccountTokensAcrossChainsUnified,
404+
);
405+
385406
(
386407
selectPooledStakingEnabledFlag as jest.MockedFunction<
387408
typeof selectPooledStakingEnabledFlag
@@ -546,6 +567,113 @@ describe('Earn Controller Selectors', () => {
546567
zeroBalances,
547568
);
548569
});
570+
571+
describe('trxNativeTokenAddress', () => {
572+
let mockHasMinimumRequiredVersion: jest.SpyInstance;
573+
574+
const createTronToken = (overrides: Partial<TokenI> = {}): TokenI =>
575+
({
576+
address: TRX_NATIVE_TOKEN_ADDRESS,
577+
chainId: TRON_MAINNET_CHAIN_ID,
578+
symbol: 'TRX',
579+
name: 'TRON',
580+
decimals: 6,
581+
isNative: true,
582+
isETH: false,
583+
isStaked: false,
584+
balance: '100',
585+
balanceFiat: '12',
586+
aggregators: [],
587+
ticker: 'TRX',
588+
...overrides,
589+
}) as TokenI;
590+
591+
const createStateWithTrxStakingEnabled = () =>
592+
({
593+
...mockState,
594+
engine: {
595+
...mockState.engine,
596+
backgroundState: {
597+
...mockState.engine.backgroundState,
598+
RemoteFeatureFlagController: {
599+
remoteFeatureFlags: {
600+
trxStakingEnabled: { enabled: true, minimumVersion: '1.0.0' },
601+
},
602+
cacheTimestamp: 0,
603+
},
604+
EarnController: {
605+
...mockState.engine.backgroundState.EarnController,
606+
lending: {
607+
markets: [],
608+
positions: [],
609+
},
610+
},
611+
},
612+
},
613+
}) as unknown as RootState;
614+
615+
beforeEach(() => {
616+
mockHasMinimumRequiredVersion = jest.spyOn(
617+
remoteFeatureFlagModule,
618+
'hasMinimumRequiredVersion',
619+
);
620+
mockHasMinimumRequiredVersion.mockReturnValue(true);
621+
(getVersion as jest.MockedFunction<typeof getVersion>).mockReturnValue(
622+
'1.0.0',
623+
);
624+
625+
(
626+
selectPooledStakingEnabledFlag as jest.MockedFunction<
627+
typeof selectPooledStakingEnabledFlag
628+
>
629+
).mockReturnValue(false);
630+
(
631+
selectStablecoinLendingEnabledFlag as jest.MockedFunction<
632+
typeof selectStablecoinLendingEnabledFlag
633+
>
634+
).mockReturnValue(false);
635+
});
636+
637+
afterEach(() => {
638+
mockHasMinimumRequiredVersion?.mockRestore();
639+
});
640+
641+
it('includes canonical TRX native token as a pooled staking earn token', () => {
642+
const tronToken = createTronToken();
643+
644+
mockSelectAccountTokensAcrossChainsUnified.mockReturnValue({
645+
[TRON_MAINNET_CHAIN_ID]: [tronToken],
646+
});
647+
648+
const result = earnSelectors.selectEarnTokens(
649+
createStateWithTrxStakingEnabled(),
650+
);
651+
652+
expect(result.earnTokens).toHaveLength(1);
653+
expect(result.earnTokens[0].address).toBe(TRX_NATIVE_TOKEN_ADDRESS);
654+
expect(result.earnTokens[0].experience.type).toBe(
655+
EARN_EXPERIENCES.POOLED_STAKING,
656+
);
657+
});
658+
659+
it('excludes Tron slip44 tokens that do not match trxNativeTokenAddress', () => {
660+
const tronToken = createTronToken({
661+
address: 'tron:728126428/slip44:195-in-lock-period',
662+
symbol: 'TRX-IN-LOCK-PERIOD',
663+
ticker: 'TRX-IN-LOCK-PERIOD',
664+
});
665+
666+
mockSelectAccountTokensAcrossChainsUnified.mockReturnValue({
667+
[TRON_MAINNET_CHAIN_ID]: [tronToken],
668+
});
669+
670+
const result = earnSelectors.selectEarnTokens(
671+
createStateWithTrxStakingEnabled(),
672+
);
673+
674+
expect(result.earnTokens).toHaveLength(0);
675+
});
676+
});
549677
});
550678

551679
describe('selectEarnToken', () => {

app/selectors/earnController/earn/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import { isTronChainId, isNonEvmChainId } from '../../../core/Multichain/utils';
4242

4343
import { selectTrxStakingEnabled } from '../../featureFlagController/trxStakingEnabled';
4444

45+
const trxNativeTokenAddress = 'tron:728126428/slip44:195';
46+
4547
/**
4648
* Get the APR for pooled staking based on token type.
4749
* This helper centralizes APR logic to avoid scattered conditionals.
@@ -211,8 +213,7 @@ const selectEarnTokens = createDeepEqualSelector(
211213

212214
const isNonEvmNative =
213215
token.isNative && isNonEvmChainId(String(token.chainId));
214-
const isTronNative =
215-
token.isNative && isTronChainId(token.chainId as Hex);
216+
const isTronNative = token.address === trxNativeTokenAddress;
216217
const isTronStakedToken =
217218
token.isStaked &&
218219
isTronChainId(token.chainId as Hex) &&

0 commit comments

Comments
 (0)