Skip to content

Fix/sort initially fetched txes - LW-12194 #1579

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions packages/wallet/src/services/TransactionsTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,8 @@ const fetchInitialTransactions = async (
if (transactions.length === 0) {
return [];
}

return transactions.slice(0, historicalTransactionsFetchLimit);
// allTransactionsByAddresses returns in `asc` order
return transactions.slice(-1 * historicalTransactionsFetchLimit);
};

const findIntersectionAndUpdateTxStore = ({
Expand Down Expand Up @@ -335,11 +335,12 @@ const findIntersectionAndUpdateTxStore = ({
localTx.blockHeader = newTx.blockHeader;
}
}
// Skip overlapping transactions to avoid duplicates
// Skip overlapping transactions to avoid duplicates.
// Limit # of stored transactions to last `historicalTransactionsFetchLimit`
localTransactions = deduplicateSortedArray(
[...localTransactions, ...newTransactions.slice(localTxsFromSameBlock.length)],
txEquals
);
).slice(-1 * historicalTransactionsFetchLimit);
store.setAll(localTransactions);
} else if (rollbackOcurred) {
// This case handles rollbacks without new additions
Expand Down
74 changes: 51 additions & 23 deletions packages/wallet/test/services/TransactionsTracker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,12 @@ const generateRandomLetters = (length: number) => {
return result;
};


const updateTransactionIds = (transactions: Cardano.HydratedTx[]) =>
transactions.map((tx) => ({
...tx,
id: Cardano.TransactionId(`${generateRandomLetters(64)}`)
}));


describe('TransactionsTracker', () => {
const logger = dummyLogger;
const historicalTransactionsFetchLimit = 3;
Expand Down Expand Up @@ -97,7 +95,8 @@ describe('TransactionsTracker', () => {
});

it('emits empty array if store is empty and ChainHistoryProvider does not return any transactions', async () => {
chainHistoryProvider.transactionsByAddresses = jest.fn()
chainHistoryProvider.transactionsByAddresses = jest
.fn()
.mockImplementation(() => delay(50).then(() => ({ pageResults: [], totalResultCount: 0 })));
const provider$ = createAddressTransactionsProvider({
addresses$: of(addresses),
Expand All @@ -112,27 +111,41 @@ describe('TransactionsTracker', () => {
expect(store.setAll).toBeCalledTimes(0);
});

it('if store is empty, stores and emits transactions resolved by ChainHistoryProvider', async () => {
it('if store is empty, stores and emits last {historicalTransactionsFetchLimit} transactions resolved by ChainHistoryProvider', async () => {
const lowerHistoricalTransactionsFetchLimit = 2;

chainHistoryProvider.transactionsByAddresses = jest.fn().mockImplementation(() =>
delay(50).then(() => ({
...queryTransactionsResult2,
pageResults: [...queryTransactionsResult2.pageResults]
}))
);
const provider$ = createAddressTransactionsProvider({
addresses$: of(addresses),
chainHistoryProvider,
historicalTransactionsFetchLimit,
historicalTransactionsFetchLimit: lowerHistoricalTransactionsFetchLimit,
logger,
retryBackoffConfig,
store,
tipBlockHeight$
}).transactionsSource$;
expect(await firstValueFrom(provider$)).toEqual(queryTransactionsResult.pageResults);
const lastHistoricalTransactionsFetchLimitTransactions = queryTransactionsResult2.pageResults.slice(
-1 * lowerHistoricalTransactionsFetchLimit
);
expect(await firstValueFrom(provider$)).toEqual(lastHistoricalTransactionsFetchLimitTransactions);
expect(store.setAll).toBeCalledTimes(1);
expect(store.setAll).toBeCalledWith(queryTransactionsResult.pageResults);
expect(store.setAll).toBeCalledWith(lastHistoricalTransactionsFetchLimitTransactions);
});

it('emits configured number of latest historical transactions', async () => {
const totalTxsCount = PAGE_SIZE + 5;

const allTransactions = generateTxAlonzo(totalTxsCount);
chainHistoryProvider.transactionsByAddresses = jest.fn().mockImplementation(
(args: TransactionsByAddressesArgs) => filterAndPaginateTransactions(allTransactions, args));
chainHistoryProvider.transactionsByAddresses = jest
.fn()
.mockImplementation((args: TransactionsByAddressesArgs) =>
filterAndPaginateTransactions(allTransactions, args)
);

const provider$ = createAddressTransactionsProvider({
addresses$: of(addresses),
Expand Down Expand Up @@ -183,7 +196,8 @@ describe('TransactionsTracker', () => {
await firstValueFrom(store.setAll([txId1, txId2]));

// ChainHistory is shorter by 1 tx: [1]
chainHistoryProvider.transactionsByAddresses = jest.fn()
chainHistoryProvider.transactionsByAddresses = jest
.fn()
// the mismatch will pop the single transaction found in the stored transactions
.mockImplementationOnce(() => delay(50).then(() => ({ pageResults: [], totalResultCount: 0 })))
// intersection is found, chain is shortened
Expand Down Expand Up @@ -221,7 +235,8 @@ describe('TransactionsTracker', () => {
await firstValueFrom(store.setAll([txId1, txId2]));

// ChainHistory has one common and one different: [1, 3]
chainHistoryProvider.transactionsByAddresses = jest.fn()
chainHistoryProvider.transactionsByAddresses = jest
.fn()
// the mismatch will pop the single transaction found in the stored transactions
.mockImplementationOnce(() => delay(50).then(() => ({ pageResults: [txId3], totalResultCount: 1 })))
// intersection is found, and stored history is populated with the new transaction
Expand Down Expand Up @@ -263,7 +278,8 @@ describe('TransactionsTracker', () => {

it('queries ChainHistoryProvider again with blockRange lower bound from a previous transaction on rollback', async () => {
await firstValueFrom(store.setAll(queryTransactionsResult.pageResults));
chainHistoryProvider.transactionsByAddresses = jest.fn()
chainHistoryProvider.transactionsByAddresses = jest
.fn()
.mockImplementationOnce(() => delay(50).then(() => ({ pageResults: [], totalResultCount: 0 })))
.mockImplementationOnce(() => delay(50).then(() => ({ pageResults: [], totalResultCount: 0 })))
.mockImplementationOnce(() =>
Expand Down Expand Up @@ -396,7 +412,10 @@ describe('TransactionsTracker', () => {

it('ignores duplicate transactions', async () => {
// eslint-disable-next-line max-len
const [txId1, txId2, txId3] = updateTransactionsBlockNo(queryTransactionsResult2.pageResults, Cardano.BlockNo(10_050));
const [txId1, txId2, txId3] = updateTransactionsBlockNo(
queryTransactionsResult2.pageResults,
Cardano.BlockNo(10_050)
);

txId1.blockHeader.slot = Cardano.Slot(10_050);
txId2.blockHeader.slot = Cardano.Slot(10_051);
Expand Down Expand Up @@ -428,7 +447,7 @@ describe('TransactionsTracker', () => {

expect(await firstValueFrom(provider$.pipe(bufferCount(2)))).toEqual([
[txId1, txId1, txId2], // from store
[txId1, txId2, txId3] // chain history (fixes stored duplicates)
[txId1, txId2, txId3] // chain history (fixes stored duplicates)
]);
expect(rollbacks.length).toBe(0);
expect(store.setAll).toBeCalledTimes(2);
Expand Down Expand Up @@ -500,10 +519,19 @@ describe('TransactionsTracker', () => {

await firstValueFrom(store.setAll([txId1, txId2, txId3]));

chainHistoryProvider.transactionsByAddresses = jest.fn().mockImplementation(() => ({
pageResults: [txId3OtherBlock, txId1OtherBlock, txId2OtherBlock],
totalResultCount: 3
}));
chainHistoryProvider.transactionsByAddresses = jest
.fn()
.mockImplementationOnce(() => ({
// asc
pageResults: [txId3OtherBlock, txId1OtherBlock, txId2OtherBlock],
totalResultCount: 3
}))
// detects a rollback and reverts all local transactions (all in the same block)
// fetches from scratch - provider is called with 'desc' order
.mockImplementationOnce(() => ({
pageResults: [txId2OtherBlock, txId1OtherBlock, txId3OtherBlock],
totalResultCount: 3
}));

const { transactionsSource$: provider$, rollback$ } = createAddressTransactionsProvider({
addresses$: of(addresses),
Expand Down Expand Up @@ -544,11 +572,11 @@ describe('TransactionsTracker', () => {

await firstValueFrom(store.setAll([txId1, txId2]));

chainHistoryProvider.transactionsByAddresses = jest.fn().mockImplementation(
(args: TransactionsByAddressesArgs) => filterAndPaginateTransactions(
[txId1OtherBlock, txId2OtherBlock, txId3OtherBlock]
, args)
);
chainHistoryProvider.transactionsByAddresses = jest
.fn()
.mockImplementation((args: TransactionsByAddressesArgs) =>
filterAndPaginateTransactions([txId1OtherBlock, txId2OtherBlock, txId3OtherBlock], args)
);

const { transactionsSource$: provider$, rollback$ } = createAddressTransactionsProvider({
addresses$: of(addresses),
Expand Down
Loading