Skip to content

Commit 0dac0e3

Browse files
n3psCopilot
andcommitted
refactor
Co-authored-by: Copilot <copilot@github.com>
1 parent 6596ada commit 0dac0e3

4 files changed

Lines changed: 159 additions & 49 deletions

File tree

app/components/Views/UnifiedTransactionsView/UnifiedTransactionsView.tsx

Lines changed: 21 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,16 @@ import { UnifiedTransactionsViewSelectorsIDs } from './UnifiedTransactionsView.t
5757
import { useMultichainActivityMaliciousTokenKeys } from '../../hooks/useMultichainActivityMaliciousTokenKeys/useMultichainActivityMaliciousTokenKeys';
5858
import { filterMultichainTransactionsExcludingMaliciousTokenActivity } from '../../../util/multichain/multichainTransactionTokenScan';
5959
import { useTransactionsQuery } from './useTransactionsQuery';
60-
import type { EvmTransaction, TransactionViewModel } from './types';
61-
import { isBridgeHistoryForEvmTransaction } from './helpers/transformations';
60+
import {
61+
type EvmTransaction,
62+
TransactionKind,
63+
type TransactionViewModel,
64+
type UnifiedItem,
65+
} from './types';
66+
import {
67+
isBridgeHistoryForEvmTransaction,
68+
mergeTransactionsByTime,
69+
} from './helpers/transformations';
6270

6371
const confirmedEvmOverscan = 5;
6472
const visibilityConfig = { itemVisiblePercentThreshold: 1 };
@@ -72,28 +80,6 @@ const getEvmTransactionTime = (tx: EvmTransaction) => tx.time ?? 0;
7280

7381
const getEvmChainId = (tx: EvmTransaction) => tx.chainId;
7482

75-
enum TransactionKind {
76-
Evm = 'evm',
77-
ConfirmedEvm = 'confirmed',
78-
NonEvm = 'nonEvm',
79-
}
80-
81-
type UnifiedItem =
82-
| { kind: TransactionKind.Evm; tx: EvmTransaction }
83-
| { kind: TransactionKind.ConfirmedEvm; tx: TransactionViewModel }
84-
| { kind: TransactionKind.NonEvm; tx: NonEvmTransaction };
85-
86-
const normalizeTimestamp = (item: UnifiedItem) => {
87-
switch (item.kind) {
88-
case TransactionKind.Evm:
89-
return item.tx.time ?? 0;
90-
case TransactionKind.ConfirmedEvm:
91-
return item.tx.time;
92-
case TransactionKind.NonEvm:
93-
return (item.tx.timestamp ?? 0) * 1000;
94-
}
95-
};
96-
9783
const generateKey = (item: UnifiedItem) => {
9884
if (item.kind === TransactionKind.Evm) {
9985
return getTransactionId(item.tx);
@@ -192,8 +178,8 @@ const UnifiedTransactionsView = ({
192178
const bridgeHistory = useSelector(selectBridgeHistoryForAccount);
193179

194180
const unifiedTransactionSource = useMemo<{
195-
evmPendingItems: UnifiedItem[];
196-
evmConfirmedItems: UnifiedItem[];
181+
evmPendingTxs: EvmTransaction[];
182+
evmConfirmedTxs: TransactionViewModel[];
197183
chainFilteredNonEvmTransactionsForSelectedChain: NonEvmTransaction[];
198184
}>(() => {
199185
const bridgeHistoryValues = Object.values(bridgeHistory ?? {});
@@ -262,18 +248,9 @@ const UnifiedTransactionsView = ({
262248
(tx, index, self) => index === self.findIndex((t) => t.id === tx.id),
263249
);
264250

265-
const evmPendingItems: UnifiedItem[] = evmPendingFirst.map((tx) => ({
266-
kind: TransactionKind.Evm,
267-
tx,
268-
}));
269-
const evmConfirmedItems: UnifiedItem[] = allConfirmedFiltered.map((tx) => ({
270-
kind: TransactionKind.ConfirmedEvm,
271-
tx,
272-
}));
273-
274251
return {
275-
evmPendingItems,
276-
evmConfirmedItems,
252+
evmPendingTxs: evmPendingFirst,
253+
evmConfirmedTxs: allConfirmedFiltered,
277254
chainFilteredNonEvmTransactionsForSelectedChain,
278255
};
279256
}, [
@@ -290,8 +267,8 @@ const UnifiedTransactionsView = ({
290267
nonEvmTransactionsForSelectedChain: NonEvmTransaction[];
291268
}>(() => {
292269
const {
293-
evmPendingItems,
294-
evmConfirmedItems,
270+
evmPendingTxs,
271+
evmConfirmedTxs,
295272
chainFilteredNonEvmTransactionsForSelectedChain,
296273
} = unifiedTransactionSource;
297274

@@ -301,18 +278,14 @@ const UnifiedTransactionsView = ({
301278
maliciousTokenKeys,
302279
);
303280

304-
const nonEvmItems: UnifiedItem[] =
305-
filteredNonEvmTransactionsForSelectedChain.map((tx) => ({
306-
kind: TransactionKind.NonEvm,
307-
tx,
308-
}));
309-
310-
const confirmedUnified = [...evmConfirmedItems, ...nonEvmItems].sort(
311-
(a, b) => normalizeTimestamp(b) - normalizeTimestamp(a),
281+
const data = mergeTransactionsByTime(
282+
evmPendingTxs,
283+
evmConfirmedTxs,
284+
filteredNonEvmTransactionsForSelectedChain,
312285
);
313286

314287
return {
315-
data: [...evmPendingItems, ...confirmedUnified],
288+
data,
316289
nonEvmTransactionsForSelectedChain:
317290
filteredNonEvmTransactionsForSelectedChain,
318291
};

app/components/Views/UnifiedTransactionsView/helpers/transformations.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@ import {
33
type V4MultiAccountTransactionsResponse,
44
} from '@metamask/core-backend';
55
import type { BridgeHistoryItem } from '@metamask/bridge-status-controller';
6+
import type { Transaction as NonEvmTransaction } from '@metamask/keyring-api';
67
import type { InfiniteData } from '@tanstack/react-query';
78
import { normalizeTransaction } from './adapters';
8-
import { EvmTransaction, TransactionViewModel } from '../types';
9+
import {
10+
EvmTransaction,
11+
TransactionKind,
12+
TransactionViewModel,
13+
UnifiedItem,
14+
} from '../types';
915
import { equalsIgnoreCase } from '../../../../util/string';
1016

1117
const excludedTransactionTypes = ['SPAM_TOKEN_TRANSFER'];
@@ -149,3 +155,60 @@ export function selectTransactions({ address }: { address: string }) {
149155
})),
150156
});
151157
}
158+
159+
const getEvmTime = (tx: EvmTransaction) => tx.time ?? 0;
160+
const getNonEvmTime = (tx: NonEvmTransaction) => (tx.timestamp ?? 0) * 1000;
161+
const getEvmHash = (tx: EvmTransaction) =>
162+
'hash' in tx && typeof tx.hash === 'string' ? tx.hash.toLowerCase() : '';
163+
164+
// Merges local EVM, API-confirmed EVM and non-EVM transactions into one list
165+
// sorted by time (newest first), deduplicated by hash (API-confirmed wins).
166+
export function mergeTransactionsByTime(
167+
evmLocalTransactions: EvmTransaction[],
168+
evmConfirmedTransactions: TransactionViewModel[],
169+
nonEvmTransactions: NonEvmTransaction[],
170+
) {
171+
const seenHashes = new Set<string>();
172+
173+
const confirmedItems: UnifiedItem[] = [];
174+
for (const tx of evmConfirmedTransactions) {
175+
const hash = tx.hash?.toLowerCase();
176+
if (hash) {
177+
if (seenHashes.has(hash)) {
178+
continue;
179+
}
180+
seenHashes.add(hash);
181+
}
182+
confirmedItems.push({
183+
kind: TransactionKind.ConfirmedEvm,
184+
tx,
185+
time: tx.time ?? 0,
186+
});
187+
}
188+
189+
const localItems: UnifiedItem[] = [];
190+
for (const tx of evmLocalTransactions) {
191+
const hash = getEvmHash(tx);
192+
if (hash) {
193+
if (seenHashes.has(hash)) {
194+
continue;
195+
}
196+
seenHashes.add(hash);
197+
}
198+
localItems.push({
199+
kind: TransactionKind.Evm,
200+
tx,
201+
time: getEvmTime(tx),
202+
});
203+
}
204+
205+
const nonEvmItems: UnifiedItem[] = nonEvmTransactions.map((tx) => ({
206+
kind: TransactionKind.NonEvm,
207+
tx,
208+
time: getNonEvmTime(tx),
209+
}));
210+
211+
return [...localItems, ...confirmedItems, ...nonEvmItems].sort(
212+
(a, b) => b.time - a.time,
213+
);
214+
}

app/components/Views/UnifiedTransactionsView/types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { V1TransactionByHashResponse } from '@metamask/core-backend';
22
import { SmartTransaction } from '@metamask/smart-transactions-controller';
33
import type { TransactionMeta } from '@metamask/transaction-controller';
4+
import type { Transaction as NonEvmTransaction } from '@metamask/keyring-api';
45

56
export type SmartTransactionWithId = SmartTransaction & { id: string };
67

@@ -14,3 +15,18 @@ export type TransactionViewModel = V1TransactionByHashResponse & {
1415
// But for now, we keep this until we can refactor the UI components
1516
transactionMeta: TransactionMeta;
1617
};
18+
19+
export enum TransactionKind {
20+
Evm = 'evm',
21+
ConfirmedEvm = 'confirmed',
22+
NonEvm = 'nonEvm',
23+
}
24+
25+
export type UnifiedItem =
26+
| { kind: TransactionKind.Evm; tx: EvmTransaction; time: number }
27+
| {
28+
kind: TransactionKind.ConfirmedEvm;
29+
tx: TransactionViewModel;
30+
time: number;
31+
}
32+
| { kind: TransactionKind.NonEvm; tx: NonEvmTransaction; time: number };

app/core/apiClient.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { createApiPlatformClient } from '@metamask/core-backend';
2+
import Engine from './Engine';
3+
4+
jest.mock('@metamask/core-backend', () => ({
5+
createApiPlatformClient: jest.fn(() => ({ accounts: {} })),
6+
}));
7+
8+
jest.mock('./Engine', () => ({
9+
__esModule: true,
10+
default: {
11+
context: {
12+
AuthenticationController: {
13+
getBearerToken: jest.fn(),
14+
},
15+
},
16+
},
17+
}));
18+
19+
const createApiPlatformClientMock = jest.mocked(createApiPlatformClient);
20+
const getBearerTokenMock = jest.mocked(
21+
Engine.context.AuthenticationController.getBearerToken,
22+
);
23+
24+
// Import once so the module-level createApiPlatformClient call is captured.
25+
require('./apiClient');
26+
27+
const [firstCallArgs] = createApiPlatformClientMock.mock.calls;
28+
const getBearerToken = firstCallArgs[0].getBearerToken as () => Promise<
29+
string | undefined
30+
>;
31+
32+
describe('apiClient', () => {
33+
beforeEach(() => {
34+
getBearerTokenMock.mockReset();
35+
});
36+
37+
it('creates the API platform client with the mobile product identifier', () => {
38+
expect(createApiPlatformClientMock).toHaveBeenCalledWith(
39+
expect.objectContaining({
40+
clientProduct: 'metamask-mobile',
41+
getBearerToken: expect.any(Function),
42+
}),
43+
);
44+
});
45+
46+
it('returns the bearer token from AuthenticationController', async () => {
47+
getBearerTokenMock.mockResolvedValueOnce('bearer-token-mock');
48+
49+
await expect(getBearerToken()).resolves.toBe('bearer-token-mock');
50+
expect(getBearerTokenMock).toHaveBeenCalled();
51+
});
52+
53+
it('returns undefined when AuthenticationController throws', async () => {
54+
getBearerTokenMock.mockRejectedValueOnce(new Error('boom'));
55+
56+
await expect(getBearerToken()).resolves.toBeUndefined();
57+
});
58+
});

0 commit comments

Comments
 (0)