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..4d179e49b44 100644 --- a/app/selectors/rampsController/index.test.ts +++ b/app/selectors/rampsController/index.test.ts @@ -6,6 +6,13 @@ import { type Country, 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, @@ -14,6 +21,7 @@ import { selectPaymentMethods, selectRampsControllerState, selectRampsOrders, + selectRampsOrdersForSelectedAccountGroup, selectTransak, } from './index'; @@ -31,6 +39,7 @@ type RampsControllerStateOverride = Partial; const createMockState = ( rampsController: RampsControllerStateOverride = {}, + extraBackgroundState: Record = {}, ): RootState => ({ engine: { @@ -58,10 +67,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 +378,198 @@ 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([]); + }); + + 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', () => { 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). */