Skip to content

Commit 856e82e

Browse files
committed
Cache timestamp until cursor available
Query previous day if entire history disabled.
1 parent 223fa39 commit 856e82e

File tree

3 files changed

+180
-21
lines changed

3 files changed

+180
-21
lines changed

packages/transaction-controller/src/helpers/AccountsApiRemoteTransactionSource.test.ts

+75-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ jest.mock('../api/accounts-api');
1414
jest.useFakeTimers();
1515

1616
const ADDRESS_MOCK = '0x123';
17-
const NOW_MOCK = 789000;
17+
const ONE_DAY_MS = 1000 * 60 * 60 * 24;
18+
const NOW_MOCK = 789000 + ONE_DAY_MS;
1819
const CURSOR_MOCK = 'abcdef';
20+
const CACHED_TIMESTAMP_MOCK = 456;
21+
const INITIAL_TIMESTAMP_MOCK = 789;
1922

2023
const REQUEST_MOCK: RemoteTransactionSourceRequest = {
2124
address: ADDRESS_MOCK,
@@ -141,7 +144,7 @@ describe('AccountsApiRemoteTransactionSource', () => {
141144
expect(getAccountTransactionsMock).toHaveBeenCalledTimes(1);
142145
expect(getAccountTransactionsMock).toHaveBeenCalledWith(
143146
expect.objectContaining({
144-
startTimestamp: 789,
147+
startTimestamp: INITIAL_TIMESTAMP_MOCK,
145148
}),
146149
);
147150
});
@@ -163,6 +166,24 @@ describe('AccountsApiRemoteTransactionSource', () => {
163166
);
164167
});
165168

169+
it('queries accounts API with timestamp from cache', async () => {
170+
await new AccountsApiRemoteTransactionSource().fetchTransactions({
171+
...REQUEST_MOCK,
172+
queryEntireHistory: false,
173+
cache: {
174+
[`accounts-api#timestamp#${SUPPORTED_CHAIN_IDS.join(',')}#${ADDRESS_MOCK}`]:
175+
CACHED_TIMESTAMP_MOCK,
176+
},
177+
});
178+
179+
expect(getAccountTransactionsMock).toHaveBeenCalledTimes(1);
180+
expect(getAccountTransactionsMock).toHaveBeenCalledWith(
181+
expect.objectContaining({
182+
startTimestamp: CACHED_TIMESTAMP_MOCK,
183+
}),
184+
);
185+
});
186+
166187
it('returns normalized standard transaction', async () => {
167188
getAccountTransactionsMock.mockResolvedValue({
168189
data: [RESPONSE_STANDARD_MOCK],
@@ -250,6 +271,58 @@ describe('AccountsApiRemoteTransactionSource', () => {
250271
});
251272
});
252273

274+
it('removes timestamp cache entry if response has cursor', async () => {
275+
getAccountTransactionsMock.mockResolvedValueOnce({
276+
data: [RESPONSE_STANDARD_MOCK],
277+
pageInfo: { hasNextPage: false, count: 1, cursor: CURSOR_MOCK },
278+
});
279+
280+
const cacheMock = {
281+
[`accounts-api#timestamp#${SUPPORTED_CHAIN_IDS.join(',')}#${ADDRESS_MOCK}`]:
282+
CACHED_TIMESTAMP_MOCK,
283+
};
284+
285+
const updateCacheMock = jest
286+
.fn()
287+
.mockImplementation((fn) => fn(cacheMock));
288+
289+
await new AccountsApiRemoteTransactionSource().fetchTransactions({
290+
...REQUEST_MOCK,
291+
updateCache: updateCacheMock,
292+
});
293+
294+
expect(updateCacheMock).toHaveBeenCalledTimes(1);
295+
expect(cacheMock).toStrictEqual({
296+
[`accounts-api#${SUPPORTED_CHAIN_IDS.join(',')}#${ADDRESS_MOCK}`]:
297+
CURSOR_MOCK,
298+
});
299+
});
300+
301+
it('updates cache with timestamp if response does not have cursor', async () => {
302+
getAccountTransactionsMock.mockResolvedValueOnce({
303+
data: [],
304+
pageInfo: { hasNextPage: false, count: 0, cursor: undefined },
305+
});
306+
307+
const cacheMock = {};
308+
309+
const updateCacheMock = jest
310+
.fn()
311+
.mockImplementation((fn) => fn(cacheMock));
312+
313+
await new AccountsApiRemoteTransactionSource().fetchTransactions({
314+
...REQUEST_MOCK,
315+
queryEntireHistory: false,
316+
updateCache: updateCacheMock,
317+
});
318+
319+
expect(updateCacheMock).toHaveBeenCalledTimes(1);
320+
expect(cacheMock).toStrictEqual({
321+
[`accounts-api#timestamp#${SUPPORTED_CHAIN_IDS.join(',')}#${ADDRESS_MOCK}`]:
322+
INITIAL_TIMESTAMP_MOCK,
323+
});
324+
});
325+
253326
it('ignores outgoing transactions if updateTransactions is false', async () => {
254327
getAccountTransactionsMock.mockResolvedValue({
255328
data: [{ ...RESPONSE_STANDARD_MOCK, to: '0x456' }],

packages/transaction-controller/src/helpers/AccountsApiRemoteTransactionSource.ts

+104-18
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import type {
1818
} from '../types';
1919
import { TransactionStatus, TransactionType } from '../types';
2020

21+
const RECENT_HISTORY_DURATION_MS = 1000 * 60 * 60 * 24; // 1 Day
22+
2123
export const SUPPORTED_CHAIN_IDS: Hex[] = [
2224
CHAIN_IDS.MAINNET,
2325
CHAIN_IDS.POLYGON,
@@ -84,32 +86,49 @@ export class AccountsApiRemoteTransactionSource
8486

8587
const cursor = this.#getCacheCursor(cache, SUPPORTED_CHAIN_IDS, address);
8688

89+
const timestamp = this.#getCacheTimestamp(
90+
cache,
91+
SUPPORTED_CHAIN_IDS,
92+
address,
93+
);
94+
8795
if (cursor) {
8896
log('Using cached cursor', cursor);
97+
} else if (timestamp) {
98+
log('Using cached timestamp', timestamp);
99+
} else {
100+
log('No cached cursor or timestamp found');
89101
}
90102

91-
return await this.#queryTransactions(request, SUPPORTED_CHAIN_IDS, cursor);
103+
return await this.#queryTransactions(
104+
request,
105+
SUPPORTED_CHAIN_IDS,
106+
cursor,
107+
timestamp,
108+
);
92109
}
93110

94111
async #queryTransactions(
95112
request: RemoteTransactionSourceRequest,
96113
chainIds: Hex[],
97114
cursor?: string,
115+
timestamp?: number,
98116
): Promise<TransactionResponse[]> {
99-
const { address, queryEntireHistory, updateCache } = request;
117+
const { address, queryEntireHistory } = request;
100118
const transactions: TransactionResponse[] = [];
101119

102120
let hasNextPage = true;
103121
let currentCursor = cursor;
104122
let pageCount = 0;
105123

106-
const startTimestamp =
107-
queryEntireHistory || cursor
108-
? undefined
109-
: this.#getTimestampSeconds(Date.now());
110-
111124
while (hasNextPage) {
112125
try {
126+
const startTimestamp = this.#getStartTimestamp({
127+
cursor: currentCursor,
128+
queryEntireHistory,
129+
timestamp,
130+
});
131+
113132
const response = await getAccountTransactions({
114133
address,
115134
chainIds,
@@ -127,15 +146,12 @@ export class AccountsApiRemoteTransactionSource
127146
hasNextPage = response?.pageInfo?.hasNextPage;
128147
currentCursor = response?.pageInfo?.cursor;
129148

130-
if (currentCursor) {
131-
// eslint-disable-next-line no-loop-func
132-
updateCache((cache) => {
133-
const key = this.#getCacheKey(chainIds, address);
134-
cache[key] = currentCursor;
135-
136-
log('Updated cache', { key, newCursor: currentCursor });
137-
});
138-
}
149+
this.#updateCache({
150+
chainIds,
151+
cursor: currentCursor,
152+
request,
153+
startTimestamp,
154+
});
139155
} catch (error) {
140156
log('Error while fetching transactions', error);
141157
break;
@@ -248,7 +264,64 @@ export class AccountsApiRemoteTransactionSource
248264
};
249265
}
250266

251-
#getCacheKey(chainIds: Hex[], address: Hex): string {
267+
#updateCache({
268+
chainIds,
269+
cursor,
270+
request,
271+
startTimestamp,
272+
}: {
273+
chainIds: Hex[];
274+
cursor?: string;
275+
request: RemoteTransactionSourceRequest;
276+
startTimestamp?: number;
277+
}) {
278+
if (!cursor && !startTimestamp) {
279+
log('Cache not updated');
280+
return;
281+
}
282+
283+
const { address, updateCache } = request;
284+
const cursorCacheKey = this.#getCursorCacheKey(chainIds, address);
285+
const timestampCacheKey = this.#getTimestampCacheKey(chainIds, address);
286+
287+
updateCache((cache) => {
288+
if (cursor) {
289+
cache[cursorCacheKey] = cursor;
290+
delete cache[timestampCacheKey];
291+
292+
log('Updated cursor in cache', { cursorCacheKey, newCursor: cursor });
293+
} else {
294+
cache[timestampCacheKey] = startTimestamp;
295+
296+
log('Updated timestamp in cache', {
297+
timestampCacheKey,
298+
newTimestamp: startTimestamp,
299+
});
300+
}
301+
});
302+
}
303+
304+
#getStartTimestamp({
305+
cursor,
306+
queryEntireHistory,
307+
timestamp,
308+
}: {
309+
cursor?: string;
310+
queryEntireHistory: boolean;
311+
timestamp?: number;
312+
}): number | undefined {
313+
if (queryEntireHistory || cursor) {
314+
return undefined;
315+
}
316+
317+
if (timestamp) {
318+
return timestamp;
319+
}
320+
321+
return this.#getTimestampSeconds(Date.now() - RECENT_HISTORY_DURATION_MS);
322+
}
323+
324+
#getCursorCacheKey(chainIds: Hex[], address: Hex): string {
252325
return `accounts-api#${chainIds.join(',')}#${address}`;
253326
}
254327

@@ -257,10 +330,23 @@ export class AccountsApiRemoteTransactionSource
257330
chainIds: Hex[],
258331
address: Hex,
259332
): string | undefined {
260-
const key = this.#getCacheKey(chainIds, address);
333+
const key = this.#getCursorCacheKey(chainIds, address);
261334
return cache[key] as string | undefined;
262335
}
263336

337+
#getTimestampCacheKey(chainIds: Hex[], address: Hex): string {
338+
return `accounts-api#timestamp#${chainIds.join(',')}#${address}`;
339+
}
340+
341+
#getCacheTimestamp(
342+
cache: Record<string, unknown>,
343+
chainIds: Hex[],
344+
address: Hex,
345+
): number | undefined {
346+
const key = this.#getTimestampCacheKey(chainIds, address);
347+
return cache[key] as number | undefined;
348+
}
349+
264350
#getTimestampSeconds(timestampMs: number): number {
265351
return Math.floor(timestampMs / 1000);
266352
}

packages/transaction-controller/src/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -922,7 +922,7 @@ export interface RemoteTransactionSourceRequest {
922922
address: Hex;
923923

924924
/**
925-
* Numerical cache to optimize fetching transactions.
925+
* Cache to optimize fetching transactions.
926926
*/
927927
cache: Record<string, unknown>;
928928

0 commit comments

Comments
 (0)