From 68407478707acfcfa426805f50743d4a4d876c28 Mon Sep 17 00:00:00 2001
From: Yu-Gi-Oh! <54774811+imyugioh@users.noreply.github.com>
Date: Tue, 24 Mar 2026 12:51:42 +0000
Subject: [PATCH 1/2] chore(runway): cherry-pick fix(ramps): filter activity
tab's transfer details for selected account -> cp-7.71.0 (#27830)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
### Activity — on-ramp orders scoped to the selected account
The Activity **Orders** tab merges legacy fiat orders with V2
`RampsController` orders. Legacy rows were already limited to the
**selected account group** via `getOrders`. V2 orders were not filtered,
so purchase history from other wallets could appear.
This change adds `selectRampsOrdersForSelectedAccountGroup`, which keeps
only orders whose `walletAddress` matches any formatted address in the
selected account group (same semantics as legacy, using
`areAddressesEqual` for EVM vs non-EVM). Hook and modal consumers that
should reflect “current wallet context” now use this selector instead of
the raw controller list.
### Transak — preserve user-entered fiat amount on Build Quote (in-app)
After **additional verification**, opening the KYC/payment webview
called `navigateToKycWebview` with **`quote.fiatAmount`**, and the stack
reset rewrote **RampAmountInput** params with that value. The quote
total can differ from what the user typed (e.g. fees), so Build Quote
could show **27.37** after the user entered **25**, and that value
persisted after closing the sheet or going back.
**Change:** `routeAfterAuthentication(..., amount)` already carried the
typed fiat; `navigateToAdditionalVerificationCallback` now puts the same
`amount` on **both** `RampAmountInput` and `RampAdditionalVerification`
route params. **V2 Additional Verification** reads that param and passes
**only** it into `navigateToKycWebview` (no `quote.fiatAmount` for this
purpose).
**Out of scope:** Order detail screens and stored order payloads are
unchanged. The **Transak payment webview** URL is unchanged (no
`fiatAmount` override in widget params); only in-app Build Quote / stack
state matches the user’s input.
## **Changelog**
CHANGELOG entry: Fixed Activity on-ramp (Orders) list showing V2
purchases from wallets other than the selected account group; fixed
Transak unified buy flow so the fiat amount on Build Quote after
additional verification matches the user-entered amount on the amount
screen (in-app stack), without changing Transak’s payment widget totals.
### Tests
- `selectRampsOrdersForSelectedAccountGroup` and ramp hook consumers
(selector + hook unit tests).
- `useTransakRouting`: IDPROOF / additional verification navigation with
user `amount` set and omitted.
- V2 `AdditionalVerification`: continue passes route `amount` into
`navigateToKycWebview`.
## **Related issues**
Refs:
[TRAM-3361](https://consensyssoftware.atlassian.net/browse/TRAM-3361)
## **Manual testing steps**
```gherkin
Feature: Activity on-ramp orders and Transak amount (TRAM-3361)
Scenario: Orders tab shows only selected account group’s V2 on-ramp orders
Given the user has two wallets (or account groups) with separate on-ramp purchase history
And unified ramps V2 orders exist for more than one wallet address
When the user selects account group A and opens Activity → Orders
Then only on-ramp orders whose destination wallet belongs to account group A are listed
When the user switches to account group B
Then the Orders tab lists only orders for account group B
Scenario: Custom fiat amount survives Transak additional verification on Build Quote
Given the user is in unified Buy with Transak (native) as provider
And the user enters a custom fiat amount (e.g. 25) on the amount screen
When the user continues through flows that require additional verification
And the user taps Continue on the additional verification screen
Then Build Quote under the KYC/payment sheet still shows the entered amount (e.g. 25), not only the quote total
When the user closes the webview or goes back from the flow
Then the amount screen still shows the same entered amount (e.g. 25)
```
## **Screenshots/Recordings**
### **Before**
https://github.com/user-attachments/assets/b26bb3cb-8219-48a3-8128-7c79026fdd18
### **After**
https://github.com/user-attachments/assets/3e1929e3-7e1a-42c9-98bd-ea8f7bc9b1eb
https://github.com/user-attachments/assets/ef6d14ed-6533-4e04-a5a4-8cbd44477170
## **Pre-merge author checklist**
- [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
- [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] 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.
## **Pre-merge reviewer checklist**
- [x] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [x] 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.
---
> [!NOTE]
> **Medium Risk**
> Moderate UX/data-scoping change: filters which on-ramp orders appear
based on multichain account selection and adjusts Transak navigation
params, which could hide expected history or affect flow state if
account/address resolution is off.
>
> **Overview**
> Fixes unified ramp UI to **scope V2 `RampsController` orders to the
selected account group**, preventing purchases from other wallets from
showing up in Activity-derived surfaces. This introduces
`selectRampsOrdersForSelectedAccountGroup` (address-matched via
`areAddressesEqual`) and switches key consumers (`useRampsOrders`,
`useRampsProviders`, `useRampsButtonClickData`,
`ProviderSelectionModal`) from the unfiltered selector.
>
> Updates the Transak additional-verification flow to **preserve the
user-entered fiat amount** through stack resets: `useTransakRouting` now
carries `amount` into `RampAdditionalVerification` params, and
`AdditionalVerification` uses that param (not `quote.fiatAmount`) when
opening the KYC webview. Selector and routing behavior are covered with
expanded unit tests.
>
> Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
39d5861d79d45c9f742c809c6a95aa1ef2aff902. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).
---
.../ProviderSelectionModal.tsx | 6 +-
.../AdditionalVerification.test.tsx | 5 +-
.../NativeFlow/AdditionalVerification.tsx | 9 +-
.../UI/Ramp/hooks/useRampsButtonClickData.ts | 6 +-
.../UI/Ramp/hooks/useRampsOrders.test.ts | 70 +++++++++-
.../UI/Ramp/hooks/useRampsOrders.ts | 4 +-
.../UI/Ramp/hooks/useRampsProviders.ts | 6 +-
.../UI/Ramp/hooks/useTransakRouting.test.ts | 59 +++++++-
.../UI/Ramp/hooks/useTransakRouting.ts | 4 +-
app/selectors/rampsController/index.test.ts | 127 ++++++++++++++++++
app/selectors/rampsController/index.ts | 34 ++++-
11 files changed, 307 insertions(+), 23 deletions(-)
diff --git a/app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/ProviderSelectionModal.tsx b/app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/ProviderSelectionModal.tsx
index b6ef67590ed..8a1d0b81bb7 100644
--- a/app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/ProviderSelectionModal.tsx
+++ b/app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/ProviderSelectionModal.tsx
@@ -17,7 +17,7 @@ import { useRampsController } from '../../../hooks/useRampsController';
import { useRampsQuotes } from '../../../hooks/useRampsQuotes';
import useRampAccountAddress from '../../../hooks/useRampAccountAddress';
import { getOrdersProviders } from '../../../../../../reducers/fiatOrders';
-import { selectRampsOrders } from '../../../../../../selectors/rampsController';
+import { selectRampsOrdersForSelectedAccountGroup } from '../../../../../../selectors/rampsController';
import { completedOrdersFromRampsOrders } from '../../../utils/determinePreferredProvider';
import { useStyles } from '../../../../../hooks/useStyles';
import styleSheet from './ProviderSelectionModal.styles';
@@ -59,7 +59,9 @@ function ProviderSelectionModal() {
} = useRampsController();
const legacyOrdersProviders = useSelector(getOrdersProviders);
- const controllerOrders = useSelector(selectRampsOrders);
+ const controllerOrders = useSelector(
+ selectRampsOrdersForSelectedAccountGroup,
+ );
const ordersProviders = useMemo(() => {
const v2ProviderIds = completedOrdersFromRampsOrders(controllerOrders).map(
diff --git a/app/components/UI/Ramp/Views/NativeFlow/AdditionalVerification.test.tsx b/app/components/UI/Ramp/Views/NativeFlow/AdditionalVerification.test.tsx
index 6375f3e7a3e..481c0cfa3c3 100644
--- a/app/components/UI/Ramp/Views/NativeFlow/AdditionalVerification.test.tsx
+++ b/app/components/UI/Ramp/Views/NativeFlow/AdditionalVerification.test.tsx
@@ -36,9 +36,10 @@ jest.mock('../../../../../util/navigation/navUtils', () => ({
(..._args: unknown[]) =>
(params: unknown) => ['MockRoute', params],
useParams: () => ({
- quote: { quoteId: 'test-quote-id', fiatAmount: 100 },
+ quote: { quoteId: 'test-quote-id', fiatAmount: 127.37 },
kycUrl: 'https://kyc.example.com',
workFlowRunId: 'wf-123',
+ amount: 25,
}),
}));
@@ -71,7 +72,7 @@ describe('V2AdditionalVerification', () => {
expect(mockNavigateToKycWebview).toHaveBeenCalledWith({
kycUrl: 'https://kyc.example.com',
- amount: 100,
+ amount: 25,
});
});
diff --git a/app/components/UI/Ramp/Views/NativeFlow/AdditionalVerification.tsx b/app/components/UI/Ramp/Views/NativeFlow/AdditionalVerification.tsx
index 190666ee128..00a4377856f 100644
--- a/app/components/UI/Ramp/Views/NativeFlow/AdditionalVerification.tsx
+++ b/app/components/UI/Ramp/Views/NativeFlow/AdditionalVerification.tsx
@@ -23,11 +23,14 @@ interface V2AdditionalVerificationParams {
quote: TransakBuyQuote;
kycUrl: string;
workFlowRunId: string;
+ /** From BuildQuote route; keeps stack amount in sync when opening KYC webview. */
+ amount?: number;
}
const V2AdditionalVerification = () => {
const navigation = useNavigation();
- const { kycUrl, quote } = useParams();
+ const { kycUrl, amount: userEnteredAmount } =
+ useParams();
const { styles, theme } = useStyles(styleSheet, {});
@@ -46,8 +49,8 @@ const V2AdditionalVerification = () => {
}, [navigation, theme]);
const handleContinuePress = useCallback(() => {
- navigateToKycWebview({ kycUrl, amount: quote?.fiatAmount });
- }, [navigateToKycWebview, kycUrl, quote?.fiatAmount]);
+ navigateToKycWebview({ kycUrl, amount: userEnteredAmount });
+ }, [navigateToKycWebview, kycUrl, userEnteredAmount]);
return (
diff --git a/app/components/UI/Ramp/hooks/useRampsButtonClickData.ts b/app/components/UI/Ramp/hooks/useRampsButtonClickData.ts
index 1868700aa6c..4b73ecb6ddf 100644
--- a/app/components/UI/Ramp/hooks/useRampsButtonClickData.ts
+++ b/app/components/UI/Ramp/hooks/useRampsButtonClickData.ts
@@ -5,7 +5,7 @@ import {
getRampRoutingDecision,
UnifiedRampRoutingType,
} from '../../../../reducers/fiatOrders';
-import { selectRampsOrders } from '../../../../selectors/rampsController';
+import { selectRampsOrdersForSelectedAccountGroup } from '../../../../selectors/rampsController';
import { getProviderToken } from '../Deposit/utils/ProviderTokenVault';
import {
completedOrdersFromFiatOrders,
@@ -21,7 +21,9 @@ export interface RampsButtonClickData {
export function useRampsButtonClickData(): RampsButtonClickData {
const orders = useSelector(getOrders);
- const controllerOrders = useSelector(selectRampsOrders);
+ const controllerOrders = useSelector(
+ selectRampsOrdersForSelectedAccountGroup,
+ );
const rampRoutingDecision = useSelector(getRampRoutingDecision);
const [isAuthenticated, setIsAuthenticated] = useState(false);
diff --git a/app/components/UI/Ramp/hooks/useRampsOrders.test.ts b/app/components/UI/Ramp/hooks/useRampsOrders.test.ts
index 0c5048f6ba5..6f1b6557191 100644
--- a/app/components/UI/Ramp/hooks/useRampsOrders.test.ts
+++ b/app/components/UI/Ramp/hooks/useRampsOrders.test.ts
@@ -2,9 +2,23 @@ import { renderHook, act } from '@testing-library/react-native';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import React from 'react';
+import { AccountGroupType } from '@metamask/account-api';
import { RampsOrderStatus, type RampsOrder } from '@metamask/ramps-controller';
+import { createMockInternalAccount } from '../../../../util/test/accountsControllerTestUtils';
import { useRampsOrders } from './useRampsOrders';
+const RAMP_HOOKS_TEST_WALLET_ID = 'keyring:use-ramps-orders-test' as const;
+const RAMP_HOOKS_TEST_GROUP_ID =
+ `${RAMP_HOOKS_TEST_WALLET_ID}/ethereum` as const;
+const RAMP_HOOKS_TEST_ACCOUNT_ID = 'account-rh-1';
+/** Must be a valid EVM address (20 bytes) so `areAddressesEqual` treats it as EVM. */
+const RAMP_HOOKS_TEST_ADDRESS = '0x2990079bcdee240329a520d2444386fc119da21a';
+
+const rampHooksTestInternalAccount = {
+ ...createMockInternalAccount(RAMP_HOOKS_TEST_ADDRESS, 'Test'),
+ id: RAMP_HOOKS_TEST_ACCOUNT_ID,
+};
+
const mockAddOrder = jest.fn();
const mockAddPrecreatedOrder = jest.fn();
const mockRemoveOrder = jest.fn();
@@ -35,7 +49,7 @@ const createMockOrder = (overrides: Partial = {}): RampsOrder => ({
createdAt: Date.now(),
totalFeesFiat: 5,
txHash: '0xabc',
- walletAddress: '0x123',
+ walletAddress: RAMP_HOOKS_TEST_ADDRESS,
status: RampsOrderStatus.Completed,
network: { name: 'Ethereum', chainId: 'eip155:1' },
canBeUpdated: false,
@@ -54,6 +68,45 @@ const createMockStore = (orders: RampsOrder[] = []) =>
RampsController: {
orders,
},
+ AccountTreeController: {
+ accountTree: {
+ wallets: {
+ [RAMP_HOOKS_TEST_WALLET_ID]: {
+ id: RAMP_HOOKS_TEST_WALLET_ID,
+ metadata: { name: 'Test wallet' },
+ groups: {
+ [RAMP_HOOKS_TEST_GROUP_ID]: {
+ id: RAMP_HOOKS_TEST_GROUP_ID,
+ type: AccountGroupType.SingleAccount,
+ accounts: [RAMP_HOOKS_TEST_ACCOUNT_ID],
+ metadata: { name: 'Test Group' },
+ },
+ },
+ },
+ },
+ selectedAccountGroup: RAMP_HOOKS_TEST_GROUP_ID,
+ },
+ },
+ RemoteFeatureFlagController: {
+ remoteFeatureFlags: {
+ enableMultichainAccounts: {
+ enabled: true,
+ featureVersion: '1',
+ minimumVersion: '1.0.0',
+ },
+ },
+ },
+ AccountsController: {
+ internalAccounts: {
+ accounts: {
+ [RAMP_HOOKS_TEST_ACCOUNT_ID]: rampHooksTestInternalAccount,
+ },
+ selectedAccount: RAMP_HOOKS_TEST_ACCOUNT_ID,
+ },
+ },
+ KeyringController: {
+ keyrings: [],
+ },
},
}),
},
@@ -78,7 +131,7 @@ describe('useRampsOrders', () => {
expect(result.current.orders).toEqual([]);
});
- it('returns orders from the store', () => {
+ it('returns orders from the store when walletAddress matches the selected account group', () => {
const order = createMockOrder();
const store = createMockStore([order]);
const { result } = renderHook(() => useRampsOrders(), {
@@ -88,6 +141,19 @@ describe('useRampsOrders', () => {
expect(result.current.orders).toEqual([order]);
});
+ it('excludes orders whose walletAddress is not in the selected account group', () => {
+ const foreignOrder = createMockOrder({
+ providerOrderId: 'foreign-order',
+ walletAddress: '0x0000000000000000000000000000000000000001',
+ });
+ const store = createMockStore([foreignOrder]);
+ const { result } = renderHook(() => useRampsOrders(), {
+ wrapper: wrapper(store),
+ });
+
+ expect(result.current.orders).toEqual([]);
+ });
+
it('finds an order by providerOrderId', () => {
const order1 = createMockOrder({ providerOrderId: 'order-1' });
const order2 = createMockOrder({ providerOrderId: 'order-2' });
diff --git a/app/components/UI/Ramp/hooks/useRampsOrders.ts b/app/components/UI/Ramp/hooks/useRampsOrders.ts
index 72f58dd8198..48a78cb0480 100644
--- a/app/components/UI/Ramp/hooks/useRampsOrders.ts
+++ b/app/components/UI/Ramp/hooks/useRampsOrders.ts
@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
import type { RampsOrder } from '@metamask/ramps-controller';
import { extractOrderCode } from '../utils/extractOrderCode';
import Engine from '../../../../core/Engine';
-import { selectRampsOrders } from '../../../../selectors/rampsController';
+import { selectRampsOrdersForSelectedAccountGroup } from '../../../../selectors/rampsController';
export interface AddPrecreatedOrderParams {
orderId: string;
@@ -31,7 +31,7 @@ export interface UseRampsOrdersResult {
}
export function useRampsOrders(): UseRampsOrdersResult {
- const orders = useSelector(selectRampsOrders);
+ const orders = useSelector(selectRampsOrdersForSelectedAccountGroup);
const getOrderById = useCallback(
(providerOrderId: string) => {
diff --git a/app/components/UI/Ramp/hooks/useRampsProviders.ts b/app/components/UI/Ramp/hooks/useRampsProviders.ts
index 255aeff8af3..017e6f626ec 100644
--- a/app/components/UI/Ramp/hooks/useRampsProviders.ts
+++ b/app/components/UI/Ramp/hooks/useRampsProviders.ts
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux';
import {
selectProviders,
- selectRampsOrders,
+ selectRampsOrdersForSelectedAccountGroup,
} from '../../../../selectors/rampsController';
import { type Provider } from '@metamask/ramps-controller';
import Engine from '../../../../core/Engine';
@@ -55,7 +55,9 @@ export function useRampsProviders(): UseRampsProvidersResult {
} = useSelector(selectProviders);
const legacyOrders = useSelector(getOrders);
- const controllerOrders = useSelector(selectRampsOrders);
+ const controllerOrders = useSelector(
+ selectRampsOrdersForSelectedAccountGroup,
+ );
const completedOrders = useMemo(
() => [
diff --git a/app/components/UI/Ramp/hooks/useTransakRouting.test.ts b/app/components/UI/Ramp/hooks/useTransakRouting.test.ts
index bde067dce25..708fcd90549 100644
--- a/app/components/UI/Ramp/hooks/useTransakRouting.test.ts
+++ b/app/components/UI/Ramp/hooks/useTransakRouting.test.ts
@@ -284,7 +284,7 @@ describe('useTransakRouting', () => {
'test-ott',
mockQuote,
MOCK_WALLET_ADDRESS,
- expect.any(Object),
+ { theme: 'light' },
);
expect(mockReset).toHaveBeenCalledWith(
expect.objectContaining({
@@ -482,10 +482,7 @@ describe('useTransakRouting', () => {
const { result } = renderHook(() => useTransakRouting());
await act(async () => {
- await result.current.routeAfterAuthentication(
- mockQuote as never,
- mockQuote.fiatAmount,
- );
+ await result.current.routeAfterAuthentication(mockQuote as never, 25);
});
expect(mockReset).toHaveBeenCalledWith(
@@ -494,7 +491,56 @@ describe('useTransakRouting', () => {
routes: [
expect.objectContaining({
name: 'RampAmountInput',
- params: { amount: mockQuote.fiatAmount },
+ params: { amount: 25 },
+ }),
+ expect.objectContaining({
+ name: 'RampAdditionalVerification',
+ params: expect.objectContaining({
+ quote: mockQuote,
+ kycUrl: 'https://kyc.example.com',
+ workFlowRunId: 'wf-123',
+ amount: 25,
+ }),
+ }),
+ ],
+ }),
+ );
+ });
+
+ it('handles ADDITIONAL_FORMS_REQUIRED with IDPROOF when user amount is omitted', async () => {
+ mockGetUserDetails.mockResolvedValue({
+ firstName: 'John',
+ address: {},
+ });
+ mockGetKycRequirement.mockResolvedValue({
+ status: 'ADDITIONAL_FORMS_REQUIRED',
+ kycType: 'STANDARD',
+ });
+ mockGetAdditionalRequirements.mockResolvedValue({
+ formsRequired: [
+ {
+ type: 'IDPROOF',
+ metadata: {
+ kycUrl: 'https://kyc.example.com',
+ workFlowRunId: 'wf-123',
+ },
+ },
+ ],
+ });
+
+ const { result } = renderHook(() => useTransakRouting());
+
+ await act(async () => {
+ await result.current.routeAfterAuthentication(mockQuote as never);
+ });
+
+ expect(mockReset).toHaveBeenCalledWith(
+ expect.objectContaining({
+ index: 1,
+ routes: [
+ expect.objectContaining({
+ name: 'RampAmountInput',
+ params: { amount: undefined },
}),
expect.objectContaining({
name: 'RampAdditionalVerification',
@@ -502,6 +548,7 @@ describe('useTransakRouting', () => {
quote: mockQuote,
kycUrl: 'https://kyc.example.com',
workFlowRunId: 'wf-123',
+ amount: undefined,
}),
}),
],
diff --git a/app/components/UI/Ramp/hooks/useTransakRouting.ts b/app/components/UI/Ramp/hooks/useTransakRouting.ts
index 661863ddf24..342e15b0441 100644
--- a/app/components/UI/Ramp/hooks/useTransakRouting.ts
+++ b/app/components/UI/Ramp/hooks/useTransakRouting.ts
@@ -40,6 +40,8 @@ interface RampStackParamList {
quote: TransakBuyQuote;
kycUrl: string;
workFlowRunId: string;
+ /** User-entered fiat from BuildQuote; used when resetting stack so amount screen keeps the typed value. */
+ amount?: number;
};
RampKycProcessing: { quote: TransakBuyQuote };
RampEnterEmail: undefined;
@@ -258,7 +260,7 @@ export const useTransakRouting = (_config?: UseTransakRoutingConfig) => {
},
{
name: Routes.RAMP.ADDITIONAL_VERIFICATION,
- params: { quote, kycUrl, workFlowRunId },
+ params: { quote, kycUrl, workFlowRunId, amount },
},
],
});
diff --git a/app/selectors/rampsController/index.test.ts b/app/selectors/rampsController/index.test.ts
index dd17c1ed4ca..ebee366ee51 100644
--- a/app/selectors/rampsController/index.test.ts
+++ b/app/selectors/rampsController/index.test.ts
@@ -6,6 +6,9 @@ import {
type Country,
type PaymentMethod,
} from '@metamask/ramps-controller';
+import { AccountGroupType } from '@metamask/account-api';
+import { InternalAccount } from '@metamask/keyring-internal-api';
+import { createMockInternalAccount } from '../../util/test/accountsControllerTestUtils';
import {
selectUserRegion,
selectProviders,
@@ -14,6 +17,7 @@ import {
selectPaymentMethods,
selectRampsControllerState,
selectRampsOrders,
+ selectRampsOrdersForSelectedAccountGroup,
selectTransak,
} from './index';
@@ -31,6 +35,7 @@ type RampsControllerStateOverride = Partial;
const createMockState = (
rampsController: RampsControllerStateOverride = {},
+ extraBackgroundState: Record = {},
): RootState =>
({
engine: {
@@ -58,10 +63,65 @@ const createMockState = (
},
...rampsController,
},
+ KeyringController: {
+ keyrings: [],
+ },
+ ...extraBackgroundState,
},
},
}) as unknown as RootState;
+const WALLET_ID = 'keyring:ramps-selector-test' as const;
+const GROUP_ID = `${WALLET_ID}/ethereum` as const;
+
+function createStateWithSelectedAccountGroup(
+ rampsController: RampsControllerStateOverride,
+ internalAccount: InternalAccount,
+ accountId: string,
+): RootState {
+ return createMockState(rampsController, {
+ AccountTreeController: {
+ accountTree: {
+ wallets: {
+ [WALLET_ID]: {
+ id: WALLET_ID,
+ metadata: { name: 'Test wallet' },
+ groups: {
+ [GROUP_ID]: {
+ id: GROUP_ID,
+ type: AccountGroupType.SingleAccount,
+ accounts: [accountId],
+ metadata: { name: 'Test Group' },
+ },
+ },
+ },
+ },
+ selectedAccountGroup: GROUP_ID,
+ },
+ },
+ RemoteFeatureFlagController: {
+ remoteFeatureFlags: {
+ enableMultichainAccounts: {
+ enabled: true,
+ featureVersion: '1',
+ minimumVersion: '1.0.0',
+ },
+ },
+ },
+ AccountsController: {
+ internalAccounts: {
+ accounts: {
+ [accountId]: internalAccount,
+ },
+ selectedAccount: accountId,
+ },
+ },
+ KeyringController: {
+ keyrings: [],
+ },
+ });
+}
+
const mockUserRegion: UserRegion = {
country: {
isoCode: 'US',
@@ -314,6 +374,73 @@ describe('RampsController Selectors', () => {
});
});
+ describe('selectRampsOrdersForSelectedAccountGroup', () => {
+ const accountId = 'account-ramps-1';
+ const walletAddrLower = '0x2990079bcdee240329a520d2444386fc119da21a';
+ const internalAccount = {
+ ...createMockInternalAccount(walletAddrLower, 'Account 1'),
+ id: accountId,
+ };
+
+ it('returns empty array when no selected account group addresses', () => {
+ const mockOrders = [
+ {
+ providerOrderId: 'order-1',
+ walletAddress: walletAddrLower,
+ status: 'COMPLETED',
+ createdAt: 1000,
+ },
+ ];
+ const state = createMockState({
+ orders: mockOrders,
+ } as never);
+
+ expect(selectRampsOrdersForSelectedAccountGroup(state)).toEqual([]);
+ });
+
+ it('keeps orders whose walletAddress matches a selected group address (case-insensitive for EVM)', () => {
+ const mockOrders = [
+ {
+ providerOrderId: 'order-match',
+ walletAddress: '0x2990079BCDEE240329A520D2444386FC119DA21A',
+ status: 'COMPLETED',
+ createdAt: 1000,
+ },
+ {
+ providerOrderId: 'order-other',
+ walletAddress: '0x0000000000000000000000000000000000000001',
+ status: 'COMPLETED',
+ createdAt: 2000,
+ },
+ ];
+ const state = createStateWithSelectedAccountGroup(
+ { orders: mockOrders } as never,
+ internalAccount,
+ accountId,
+ );
+
+ const result = selectRampsOrdersForSelectedAccountGroup(state);
+ expect(result).toEqual([mockOrders[0]]);
+ });
+
+ it('excludes orders with missing walletAddress', () => {
+ const mockOrders = [
+ {
+ providerOrderId: 'order-no-wallet',
+ status: 'COMPLETED',
+ createdAt: 1000,
+ },
+ ];
+ const state = createStateWithSelectedAccountGroup(
+ { orders: mockOrders } as never,
+ internalAccount,
+ accountId,
+ );
+
+ expect(selectRampsOrdersForSelectedAccountGroup(state)).toEqual([]);
+ });
+ });
+
describe('selectTransak', () => {
it('returns transak state when nativeProviders.transak is set', () => {
const mockTransakState = {
diff --git a/app/selectors/rampsController/index.ts b/app/selectors/rampsController/index.ts
index aa30fd4b8aa..1c86fea1d88 100644
--- a/app/selectors/rampsController/index.ts
+++ b/app/selectors/rampsController/index.ts
@@ -11,6 +11,9 @@ import {
type RampsOrder,
} from '@metamask/ramps-controller';
import { RootState } from '../../reducers';
+import { areAddressesEqual } from '../../util/address';
+import { createDeepEqualSelector } from '../util';
+import { selectSelectedAccountGroupWithInternalAccountsAddresses } from '../multichainAccounts/accountTreeController';
/**
* Selects the RampsController state from Redux.
@@ -90,13 +93,42 @@ export const selectPaymentMethods = createSelector(
);
/**
- * Selects V2 orders from RampsController state.
+ * Selects all V2 orders from RampsController state (unfiltered).
+ * For UI scoped to the selected account group, use
+ * `selectRampsOrdersForSelectedAccountGroup` instead.
*/
export const selectRampsOrders = createSelector(
selectRampsControllerState,
(rampsControllerState): RampsOrder[] => rampsControllerState?.orders ?? [],
);
+/**
+ * V2 on-ramp orders whose `walletAddress` belongs to the selected account group.
+ * Matches legacy `getOrders` scoping for fiat orders.
+ */
+export const selectRampsOrdersForSelectedAccountGroup = createDeepEqualSelector(
+ [selectRampsOrders, selectSelectedAccountGroupWithInternalAccountsAddresses],
+ (orders, addresses): RampsOrder[] => {
+ if (addresses.length === 0) {
+ return [];
+ }
+ return orders.filter((order) => {
+ const walletAddress = order.walletAddress;
+ if (!walletAddress) {
+ return false;
+ }
+ return addresses.some(
+ (addr) => addr != null && areAddressesEqual(walletAddress, addr),
+ );
+ });
+ },
+ {
+ devModeChecks: {
+ identityFunctionCheck: 'never',
+ },
+ },
+);
+
/**
* Selects the transak native provider state (isAuthenticated, userDetails, buyQuote, kycRequirement).
*/
From e31075cc2948e5992addc1cac50f9491216d6f1d Mon Sep 17 00:00:00 2001
From: imyugioh <54774811+imyugioh@users.noreply.github.com>
Date: Tue, 24 Mar 2026 08:36:26 -0500
Subject: [PATCH 2/2] chore(rampsController): add test case for solana,
bitcoin, tron
---
app/selectors/rampsController/index.test.ts | 129 ++++++++++++++++++++
1 file changed, 129 insertions(+)
diff --git a/app/selectors/rampsController/index.test.ts b/app/selectors/rampsController/index.test.ts
index ebee366ee51..4d179e49b44 100644
--- a/app/selectors/rampsController/index.test.ts
+++ b/app/selectors/rampsController/index.test.ts
@@ -7,8 +7,12 @@ import {
type PaymentMethod,
} from '@metamask/ramps-controller';
import { AccountGroupType } from '@metamask/account-api';
+import { AccountId } from '@metamask/accounts-controller';
+import { TrxAccountType } from '@metamask/keyring-api';
+import { KeyringTypes } from '@metamask/keyring-controller';
import { InternalAccount } from '@metamask/keyring-internal-api';
import { createMockInternalAccount } from '../../util/test/accountsControllerTestUtils';
+import { mockSolanaAddress } from '../../util/test/keyringControllerTestUtils';
import {
selectUserRegion,
selectProviders,
@@ -439,6 +443,131 @@ describe('RampsController Selectors', () => {
expect(selectRampsOrdersForSelectedAccountGroup(state)).toEqual([]);
});
+
+ it('keeps orders whose walletAddress matches a Solana account in the selected group', () => {
+ const solanaAccountId = 'account-ramps-solana' as AccountId;
+ const otherSolanaAddress = '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM';
+ const solanaInternalAccount: InternalAccount = {
+ id: solanaAccountId,
+ address: mockSolanaAddress,
+ type: 'solana:dataAccount' as InternalAccount['type'],
+ options: {},
+ methods: [],
+ scopes: ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'],
+ metadata: {
+ name: 'Solana Account',
+ importTime: Date.now(),
+ keyring: {
+ type: 'Snap Keyring',
+ },
+ },
+ };
+ const mockOrders = [
+ {
+ providerOrderId: 'order-sol-match',
+ walletAddress: mockSolanaAddress,
+ status: 'COMPLETED',
+ createdAt: 1000,
+ },
+ {
+ providerOrderId: 'order-sol-other',
+ walletAddress: otherSolanaAddress,
+ status: 'COMPLETED',
+ createdAt: 2000,
+ },
+ ];
+ const state = createStateWithSelectedAccountGroup(
+ { orders: mockOrders } as never,
+ solanaInternalAccount,
+ solanaAccountId,
+ );
+
+ expect(selectRampsOrdersForSelectedAccountGroup(state)).toEqual([
+ mockOrders[0],
+ ]);
+ });
+
+ it('keeps orders whose walletAddress matches a Bitcoin account in the selected group', () => {
+ const bitcoinAccountId = 'account-ramps-bitcoin' as AccountId;
+ const bitcoinAddress = 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh';
+ const otherBitcoinAddress = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq';
+ const bitcoinInternalAccount: InternalAccount = {
+ id: bitcoinAccountId,
+ address: bitcoinAddress,
+ type: 'bip122:p2wpkh' as InternalAccount['type'],
+ options: {},
+ methods: [],
+ scopes: ['bip122:000000000019d6689c085ae165831e93'],
+ metadata: {
+ name: 'Bitcoin Account',
+ importTime: Date.now(),
+ keyring: {
+ type: 'Snap Keyring',
+ },
+ },
+ };
+ const mockOrders = [
+ {
+ providerOrderId: 'order-btc-match',
+ walletAddress: bitcoinAddress,
+ status: 'COMPLETED',
+ createdAt: 1000,
+ },
+ {
+ providerOrderId: 'order-btc-other',
+ walletAddress: otherBitcoinAddress,
+ status: 'COMPLETED',
+ createdAt: 2000,
+ },
+ ];
+ const state = createStateWithSelectedAccountGroup(
+ { orders: mockOrders } as never,
+ bitcoinInternalAccount,
+ bitcoinAccountId,
+ );
+
+ expect(selectRampsOrdersForSelectedAccountGroup(state)).toEqual([
+ mockOrders[0],
+ ]);
+ });
+
+ it('keeps orders whose walletAddress matches a Tron account in the selected group', () => {
+ const tronAccountId = 'account-ramps-tron' as AccountId;
+ const tronAddress = 'TXYZopYRdj2D9XRtbPoJZ1CuXLNaoEBgD';
+ const otherTronAddress = 'TN3W4H6rK2ce4vX9YnFQHw8ENXNA9s8rPH';
+ const tronInternalAccount: InternalAccount = {
+ ...createMockInternalAccount(
+ tronAddress,
+ 'Tron Account',
+ KeyringTypes.snap,
+ TrxAccountType.Eoa,
+ ),
+ id: tronAccountId,
+ };
+ const mockOrders = [
+ {
+ providerOrderId: 'order-tron-match',
+ walletAddress: tronAddress,
+ status: 'COMPLETED',
+ createdAt: 1000,
+ },
+ {
+ providerOrderId: 'order-tron-other',
+ walletAddress: otherTronAddress,
+ status: 'COMPLETED',
+ createdAt: 2000,
+ },
+ ];
+ const state = createStateWithSelectedAccountGroup(
+ { orders: mockOrders } as never,
+ tronInternalAccount,
+ tronAccountId,
+ );
+
+ expect(selectRampsOrdersForSelectedAccountGroup(state)).toEqual([
+ mockOrders[0],
+ ]);
+ });
});
describe('selectTransak', () => {