Skip to content

Commit d607d82

Browse files
n3psCopilot
andcommitted
cleanup
Co-authored-by: Copilot <copilot@github.com>
1 parent e0230a0 commit d607d82

5 files changed

Lines changed: 115 additions & 153 deletions

File tree

app/components/Views/UnifiedTransactionsView/UnifiedTransactionsView.tsx

Lines changed: 27 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,6 @@ const UnifiedTransactionsView = ({
138138
const selectedAccountGroupInternalAccounts = useSelector(
139139
selectSelectedAccountGroupInternalAccounts,
140140
);
141-
const selectedAccountGroupInternalAccountsAddresses =
142-
selectedAccountGroupInternalAccounts.map((account) => account.address);
143141
const selectedAccountGroupEvmAddress = useMemo(() => {
144142
const evmAccount = selectedAccountGroupInternalAccounts.find(
145143
(account) =>
@@ -181,52 +179,36 @@ const UnifiedTransactionsView = ({
181179
evmConfirmedItems: UnifiedItem[];
182180
chainFilteredNonEvmTransactionsForSelectedChain: NonEvmTransaction[];
183181
}>(() => {
184-
// Deduplicate submitted by (address + chain + nonce) and drop if already confirmed
185-
const seenSubmittedNonces = new Set<string>();
186-
const submittedTxsFiltered = submittedTxs.filter((tx): tx is EvmTransaction => {
187-
if (!isEvmTransaction(tx)) {
188-
return false;
189-
}
190-
191-
const { chainId: _chainId, txParams } = tx;
192-
const hash = 'hash' in tx ? tx.hash : undefined;
193-
const { from, nonce, actionId } = txParams || {};
194-
// Some txs don't have nonce, like intent based swaps
195-
const hasNonce = nonce !== undefined && nonce !== null;
196-
if (
197-
!selectedAccountGroupInternalAccountsAddresses.some((addr) =>
198-
areAddressesEqual(from, addr),
199-
)
200-
) {
201-
return false;
202-
}
203-
const dedupeKeyPrefix = `${_chainId}-${String(from).toLowerCase()}`;
204-
const dedupeKey = hasNonce
205-
? `${dedupeKeyPrefix}-${nonce}`
206-
: `${dedupeKeyPrefix}-${actionId}`;
207-
if (seenSubmittedNonces.has(dedupeKey)) {
208-
return false;
209-
}
182+
const submittedTxsFiltered = submittedTxs.filter(
183+
(tx): tx is EvmTransaction => {
184+
if (!isEvmTransaction(tx)) {
185+
return false;
186+
}
210187

211-
const alreadyConfirmed = allConfirmedFiltered.find(
212-
(confirmedTx) =>
213-
(typeof hash === 'string' &&
214-
confirmedTx.hash.toLowerCase() === hash.toLowerCase() &&
215-
confirmedTx.txChainId === _chainId) ||
216-
(hasNonce &&
217-
confirmedTx.nonce === nonce &&
218-
confirmedTx.txChainId === _chainId &&
219-
Boolean(from) &&
220-
areAddressesEqual(confirmedTx.from, from)),
221-
);
188+
const { chainId: _chainId, txParams } = tx;
189+
const hash = 'hash' in tx ? tx.hash : undefined;
190+
const { from, nonce } = txParams || {};
191+
const hasNonce = nonce !== undefined && nonce !== null;
192+
193+
const alreadyConfirmed = allConfirmedFiltered.find(
194+
(confirmedTx) =>
195+
(typeof hash === 'string' &&
196+
confirmedTx.hash.toLowerCase() === hash.toLowerCase() &&
197+
confirmedTx.txChainId === _chainId) ||
198+
(hasNonce &&
199+
confirmedTx.nonce === nonce &&
200+
confirmedTx.txChainId === _chainId &&
201+
Boolean(from) &&
202+
areAddressesEqual(confirmedTx.from, from)),
203+
);
222204

223-
if (alreadyConfirmed) {
224-
return false;
225-
}
205+
if (alreadyConfirmed) {
206+
return false;
207+
}
226208

227-
seenSubmittedNonces.add(dedupeKey);
228-
return true;
229-
});
209+
return true;
210+
},
211+
);
230212

231213
// EVM: pending/submitted first (desc), then confirmed (dedup outgoing)
232214
const evmPendingFirst = [...submittedTxsFiltered].sort(
@@ -271,7 +253,6 @@ const UnifiedTransactionsView = ({
271253
allConfirmedFiltered,
272254
submittedTxs,
273255
nonEvmTransactions,
274-
selectedAccountGroupInternalAccountsAddresses,
275256
enabledEVMChainIds,
276257
enabledNonEVMChainIds,
277258
bridgeHistory,
@@ -590,11 +571,6 @@ const UnifiedTransactionsView = ({
590571
navigation={navigation}
591572
txChainId={item.tx.txChainId}
592573
selectedAddress={selectedInternalAccount?.address}
593-
signQRTransaction={signQRTransaction}
594-
cancelUnsignedQRTransaction={cancelUnsignedQRTransaction}
595-
isQRHardwareAccount={false}
596-
isLedgerAccount={false}
597-
signLedgerTransaction={signLedgerTransaction}
598574
currentCurrency={currentCurrency}
599575
showBottomBorder
600576
location={location}

app/components/Views/UnifiedTransactionsView/hooks/useTransactionsQuery.ts

Lines changed: 15 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,19 @@
1-
import {
2-
InfiniteData,
3-
QueryFunctionContext,
4-
QueryFunction,
5-
QueryKey,
6-
useInfiniteQuery,
7-
} from '@tanstack/react-query';
1+
import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query';
82
import { useMemo } from 'react';
93
import { useSelector } from 'react-redux';
104
import { V4MultiAccountTransactionsResponse } from '@metamask/core-backend';
11-
import { getApiClient } from '../../../../core/apiClient';
5+
import { apiClient } from '../../../../core/apiClient';
126
import { selectAccountGroupEvmAccountAddresses } from '../../../../selectors/multichainAccounts/accountTreeController';
137
import { selectEvmEnabledCaipNetworks } from '../../../../selectors/networkEnablementController';
148
import { selectConfirmedTransactions } from '../helpers/mappers';
159
import { MINUTE } from '../../../../constants/time';
16-
import type { ConfirmedEvmTransaction } from '../helpers/types';
17-
18-
type ConfirmedEvmTransactionsPage = Omit<
19-
V4MultiAccountTransactionsResponse,
20-
'data'
21-
> & {
22-
data: ConfirmedEvmTransaction[];
23-
};
2410

2511
export const useTransactionsQuery = () => {
26-
const accountAddresses = useSelector(selectAccountGroupEvmAccountAddresses);
12+
const accountAddresses = useSelector(
13+
selectAccountGroupEvmAccountAddresses,
14+
) as string[];
2715
const networks = useSelector(selectEvmEnabledCaipNetworks);
16+
2817
const selectFn = useMemo(
2918
() =>
3019
selectConfirmedTransactions({
@@ -33,49 +22,16 @@ export const useTransactionsQuery = () => {
3322
[accountAddresses],
3423
);
3524

36-
const queryOptions = useMemo(() => {
37-
const options =
38-
getApiClient().accounts.getV4MultiAccountTransactionsInfiniteQueryOptions(
39-
{
40-
accountAddresses: [...accountAddresses],
41-
networks: [...networks],
42-
includeTxMetadata: true,
43-
},
44-
);
45-
46-
return {
47-
queryKey: options.queryKey as QueryKey,
48-
queryFn: options.queryFn as QueryFunction<
49-
V4MultiAccountTransactionsResponse,
50-
QueryKey,
51-
string | undefined
52-
>,
53-
};
54-
}, [accountAddresses, networks]);
25+
const queryOptions =
26+
apiClient.accounts.getV4MultiAccountTransactionsInfiniteQueryOptions({
27+
accountAddresses,
28+
networks,
29+
includeTxMetadata: true,
30+
});
5531

56-
return useInfiniteQuery<
57-
V4MultiAccountTransactionsResponse,
58-
Error,
59-
ConfirmedEvmTransactionsPage,
60-
QueryKey
61-
>({
62-
queryKey: queryOptions.queryKey,
63-
queryFn: (({
64-
pageParam,
65-
signal,
66-
}: QueryFunctionContext<QueryKey, string | undefined>) =>
67-
queryOptions.queryFn({
68-
pageParam,
69-
signal,
70-
queryKey: queryOptions.queryKey,
71-
meta: undefined,
72-
})) as QueryFunction<
73-
V4MultiAccountTransactionsResponse,
74-
QueryKey,
75-
string | undefined
76-
>,
77-
getNextPageParam: ({ pageInfo }) =>
78-
pageInfo.hasNextPage ? pageInfo.endCursor : undefined,
32+
// @ts-expect-error apiClient returns v5 types, repo still in v4
33+
return useInfiniteQuery({
34+
...queryOptions,
7935
select: selectFn,
8036
enabled: accountAddresses.length > 0 && networks.length > 0,
8137
staleTime: 5 * MINUTE,

app/core/apiClient.ts

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,13 @@
11
import { createApiPlatformClient } from '@metamask/core-backend';
22
import Engine from './Engine';
33

4-
let apiClient: ReturnType<typeof createApiPlatformClient> | undefined;
5-
6-
export const getApiClient = () => {
7-
if (!apiClient) {
8-
apiClient = createApiPlatformClient({
9-
clientProduct: 'metamask-mobile',
10-
getBearerToken: async () => {
11-
try {
12-
return await Engine.context.AuthenticationController.getBearerToken();
13-
} catch {
14-
return undefined;
15-
}
16-
},
17-
});
18-
}
19-
20-
return apiClient;
21-
};
4+
export const apiClient = createApiPlatformClient({
5+
clientProduct: 'metamask-mobile',
6+
getBearerToken: async () => {
7+
try {
8+
return await Engine.context.AuthenticationController.getBearerToken();
9+
} catch {
10+
return undefined;
11+
}
12+
},
13+
});

app/selectors/multichainAccounts/accountTreeController.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -430,13 +430,13 @@ export const selectAccountGroupEvmAccountAddresses = createSelector(
430430
[selectSelectedInternalAccount, selectAccountGroupEvmInternalAccounts],
431431
(selectedInternalAccount, groupEvmAccounts) => {
432432
const selectedEvmAccount =
433-
selectedInternalAccount &&
434-
isEvmAccountType(selectedInternalAccount.type)
435-
? selectedInternalAccount
436-
: (groupEvmAccounts.find(
437-
(account) =>
438-
account.type === 'eip155:eoa' || account.type === 'eip155:erc4337',
439-
) ?? groupEvmAccounts[0]);
433+
selectedInternalAccount && isEvmAccountType(selectedInternalAccount.type)
434+
? selectedInternalAccount
435+
: (groupEvmAccounts.find(
436+
(account) =>
437+
account.type === 'eip155:eoa' ||
438+
account.type === 'eip155:erc4337',
439+
) ?? groupEvmAccounts[0]);
440440

441441
if (!selectedEvmAccount?.address) {
442442
return EMPTY_ARR as readonly CaipAccountId[];
@@ -470,6 +470,6 @@ export const selectSelectedAccountGroupWithInternalAccountsAddresses =
470470
}
471471
return null;
472472
})
473-
.filter(Boolean);
473+
.filter((address): address is string => Boolean(address));
474474
},
475475
);

app/selectors/transactionController.ts

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import {
55
selectPendingSmartTransactionsBySender,
66
selectPendingSmartTransactionsForSelectedAccountGroup,
77
} from './smartTransactionsController';
8-
import { selectSelectedAccountGroupInternalAccounts } from './multichainAccounts/accountTreeController';
8+
import { selectSelectedAccountGroupWithInternalAccountsAddresses } from './multichainAccounts/accountTreeController';
99
import {
1010
TransactionMeta,
1111
TransactionType,
1212
} from '@metamask/transaction-controller';
13+
import { SmartTransaction } from '@metamask/smart-transactions-controller';
1314
import { Hex } from '@metamask/utils';
1415
import { areAddressesEqual } from '../util/address';
1516

@@ -18,14 +19,53 @@ interface MetaMaskPayToken {
1819
chainId: Hex;
1920
}
2021

21-
const PENDING_EVM_TRANSACTION_STATUSES = new Set([
22+
type LocalTransaction = TransactionMeta | SmartTransaction;
23+
24+
const transactionPendingStatuses = new Set([
2225
'submitted',
2326
'signed',
2427
'unapproved',
2528
'approved',
2629
'pending',
2730
]);
2831

32+
function dedupeTransactions(
33+
transactions: LocalTransaction[],
34+
addresses: readonly string[],
35+
) {
36+
// Deduplicate submitted by (address + chain + nonce) and drop if already confirmed
37+
const seenTransactions = new Set<string>();
38+
39+
return transactions.filter((transaction) => {
40+
const { chainId, txParams } = transaction;
41+
const { from, nonce, actionId } = txParams || {};
42+
const hasNonce = nonce !== undefined && nonce !== null;
43+
44+
// Skip transactions that do not belong to one of the selected addresses
45+
if (
46+
!from ||
47+
!addresses.some((address) => areAddressesEqual(from, address || ''))
48+
) {
49+
return false;
50+
}
51+
52+
// Build a stable local dedupe key using nonce when available, or actionId for nonce-less flows
53+
// Extracted from UnifiedTransactionsView
54+
const dedupeKeyPrefix = `${chainId}-${String(from).toLowerCase()}`;
55+
const dedupeKey = hasNonce
56+
? `${dedupeKeyPrefix}-${nonce}`
57+
: `${dedupeKeyPrefix}-${actionId}`;
58+
59+
// Keep only the first local pending transaction for each dedupe key
60+
if (seenTransactions.has(dedupeKey)) {
61+
return false;
62+
}
63+
64+
seenTransactions.add(dedupeKey);
65+
return true;
66+
});
67+
}
68+
2969
function getNestedTransactionTypes(
3070
transaction: TransactionMeta,
3171
): TransactionType[] {
@@ -139,37 +179,35 @@ export const selectLocalTransactions = createDeepEqualSelector(
139179
[
140180
selectNonReplacedTransactions,
141181
selectPendingSmartTransactionsForSelectedAccountGroup,
142-
selectSelectedAccountGroupInternalAccounts,
182+
selectSelectedAccountGroupWithInternalAccountsAddresses,
143183
],
144-
(
145-
nonReplacedTransactions,
146-
pendingSmartTransactions,
147-
selectedAccountGroupInternalAccounts,
148-
) => {
149-
const selectedAddresses = selectedAccountGroupInternalAccounts
150-
.map((account) => account.address)
151-
.filter(Boolean);
152-
184+
(nonReplacedTransactions, pendingSmartTransactions, addresses) => {
153185
const pendingTransactions = nonReplacedTransactions.filter(
154186
(transaction) => {
155-
if (!PENDING_EVM_TRANSACTION_STATUSES.has(transaction.status)) {
187+
// Only keep local EVM transactions that are pending-like
188+
// Extracted from UnifiedTransactionsView submittedTxs filter
189+
if (!transactionPendingStatuses.has(transaction.status)) {
156190
return false;
157191
}
158192

193+
// We can only scope a pending transaction to the active account if it has a sender
159194
const fromAddress = transaction.txParams?.from;
160195
if (!fromAddress) {
161196
return false;
162197
}
163198

164-
return selectedAddresses.some((address) =>
199+
// Only keep pending transactions sent from one of the selected account addresses
200+
// Extracted from UnifiedTransactionsView submittedTxsFiltered filter
201+
return addresses.some((address) =>
165202
areAddressesEqual(fromAddress, address),
166203
);
167204
},
168205
);
169206

170-
return [...pendingTransactions, ...pendingSmartTransactions].sort(
171-
(a, b) => (b?.time ?? 0) - (a?.time ?? 0),
172-
);
207+
return dedupeTransactions(
208+
[...pendingTransactions, ...pendingSmartTransactions],
209+
addresses,
210+
).sort((a, b) => (b?.time ?? 0) - (a?.time ?? 0));
173211
},
174212
);
175213

0 commit comments

Comments
 (0)