Skip to content

Commit

Permalink
Merge pull request #1536 from input-output-hk/fix/transaction-tracker…
Browse files Browse the repository at this point in the history
…-now-dedups-transaction-if-needed

fix(wallet): transaction tracker now de-duplicates transactions if needed
  • Loading branch information
rhyslbw authored Nov 23, 2024
2 parents 0d9fa5f + 08e9bd1 commit 49a586b
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 7 deletions.
38 changes: 33 additions & 5 deletions packages/wallet/src/services/TransactionsTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { Logger } from 'ts-log';
import { Range, Shutdown, contextLogger } from '@cardano-sdk/util';
import { RetryBackoffConfig } from 'backoff-rxjs';
import { TrackerSubject, coldObservableProvider } from '@cardano-sdk/util-rxjs';
import { distinctBlock, signedTxsEquals, transactionsEquals, txInEquals } from './util';
import { distinctBlock, signedTxsEquals, transactionsEquals, txEquals, txInEquals } from './util';

import { WitnessedTx } from '@cardano-sdk/key-management';
import { newAndStoredMulticast } from './util/newAndStoredMulticast';
Expand Down Expand Up @@ -85,6 +85,31 @@ export const PAGE_SIZE = 25;
*/
const sortTxBySlot = (lhs: Cardano.HydratedTx, rhs: Cardano.HydratedTx) => lhs.blockHeader.slot - rhs.blockHeader.slot;

/**
* Deduplicates the given array of HydratedTx.
*
* @param arr The array of HydratedTx to deduplicate.
* @param isEqual The equality function to use to determine if two HydratedTx are equal.
*/
const deduplicateSortedArray = (
arr: Cardano.HydratedTx[],
isEqual: (a: Cardano.HydratedTx, b: Cardano.HydratedTx) => boolean
) => {
if (arr.length === 0) {
return [];
}

const result = [arr[0]];

for (let i = 1; i < arr.length; ++i) {
if (!isEqual(arr[i], arr[i - 1])) {
result.push(arr[i]);
}
}

return result;
};

const allTransactionsByAddresses = async (
chainHistoryProvider: ChainHistoryProvider,
{ addresses, blockRange }: { addresses: Cardano.PaymentAddress[]; blockRange: Range<Cardano.BlockNo> }
Expand All @@ -110,7 +135,7 @@ const allTransactionsByAddresses = async (
} while (pageResults.length === PAGE_SIZE);
}

return response.sort(sortTxBySlot);
return deduplicateSortedArray(response.sort(sortTxBySlot), txEquals);
};

const getLastTransactionsAtBlock = (
Expand Down Expand Up @@ -158,7 +183,7 @@ export const revertLastBlock = (
}
}

return result;
return deduplicateSortedArray(result, txEquals);
};

const findIntersectionAndUpdateTxStore = ({
Expand Down Expand Up @@ -251,7 +276,10 @@ const findIntersectionAndUpdateTxStore = ({

if (!areTransactionsSame) {
// Skip overlapping transactions to avoid duplicates
localTransactions = [...localTransactions, ...newTransactions.slice(localTxsFromSameBlock.length)];
localTransactions = deduplicateSortedArray(
[...localTransactions, ...newTransactions.slice(localTxsFromSameBlock.length)],
txEquals
);
store.setAll(localTransactions);
} else if (rollbackOcurred) {
// This case handles rollbacks without new additions
Expand Down Expand Up @@ -283,7 +311,7 @@ export const createAddressTransactionsProvider = (
switchMap(([addresses, storedTransactions]) =>
findIntersectionAndUpdateTxStore({
addresses,
localTransactions: [...storedTransactions],
localTransactions: deduplicateSortedArray([...storedTransactions].sort(sortTxBySlot), txEquals),
rollback$,
...props
})
Expand Down
58 changes: 56 additions & 2 deletions packages/wallet/test/services/TransactionsTracker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,24 @@ const updateTransactionsBlockNo = (transactions: Cardano.HydratedTx[], blockNo =
blockHeader: { ...tx.blockHeader, blockNo, slot: Cardano.Slot(0) }
}));

const updateTransactionIds = (transactions: Cardano.HydratedTx[], tailPattern = 'aaa') =>
const generateRandomLetters = (length: number) => {
let result = '';
const characters = '0123456789abcdef';
const charactersLength = characters.length;

for (let i = 0; i < length; ++i) {
const randomIndex = Math.floor(Math.random() * charactersLength);
result += characters.charAt(randomIndex);
}

return result;
};


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

describe('TransactionsTracker', () => {
Expand Down Expand Up @@ -378,6 +392,46 @@ describe('TransactionsTracker', () => {
expect(store.setAll).nthCalledWith(2, [txId1, txId2, txId3]);
});

it('ignores duplicate transactions', async () => {
// eslint-disable-next-line max-len
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);
txId3.blockHeader.slot = Cardano.Slot(10_052);

txId1.id = Cardano.TransactionId(generateRandomLetters(64));
txId2.id = Cardano.TransactionId(generateRandomLetters(64));
txId3.id = Cardano.TransactionId(generateRandomLetters(64));

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

chainHistoryProvider.transactionsByAddresses = jest.fn(() => ({
pageResults: [txId1, txId2, txId2, txId3, txId3, txId3],
totalResultCount: 3
}));

const { transactionsSource$: provider$, rollback$ } = createAddressTransactionsProvider({
addresses$: of(addresses),
chainHistoryProvider,
logger,
retryBackoffConfig,
store,
tipBlockHeight$
});

const rollbacks: Cardano.HydratedTx[] = [];
rollback$.subscribe((tx) => rollbacks.push(tx));

expect(await firstValueFrom(provider$.pipe(bufferCount(2)))).toEqual([
[txId1, txId1, txId2], // from store
[txId1, txId2, txId3] // chain history (fixes stored duplicates)
]);
expect(rollbacks.length).toBe(0);
expect(store.setAll).toBeCalledTimes(2);
expect(store.setAll).nthCalledWith(2, [txId1, txId2, txId3]);
});

// latestStoredBlock <1 2 3>
// newBlock <1 2>
// rollback$ 3
Expand Down

0 comments on commit 49a586b

Please sign in to comment.