From 4e23d8cbae0279d943c135ad66356962a62bfcc0 Mon Sep 17 00:00:00 2001 From: tom Date: Mon, 18 Mar 2024 16:12:49 +0300 Subject: [PATCH 1/2] Real-time incoming transactions are sorted incorrectly Fixes #1681 --- ui/address/AddressTxs.tsx | 7 ++-- ui/txs/sortTxs.test.ts | 79 +++++++++++++++++++++++++++++++++++++++ ui/txs/sortTxs.ts | 38 +++++++++++++++++++ ui/txs/useTxsSort.tsx | 20 ++-------- 4 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 ui/txs/sortTxs.test.ts create mode 100644 ui/txs/sortTxs.ts diff --git a/ui/address/AddressTxs.tsx b/ui/address/AddressTxs.tsx index cc21c355df..d272982c71 100644 --- a/ui/address/AddressTxs.tsx +++ b/ui/address/AddressTxs.tsx @@ -20,6 +20,7 @@ import Pagination from 'ui/shared/pagination/Pagination'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue'; import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery'; +import { sortTxsFromSocket } from 'ui/txs/sortTxs'; import TxsWithAPISorting from 'ui/txs/TxsWithAPISorting'; import { SORT_OPTIONS } from 'ui/txs/useTxsSort'; @@ -85,7 +86,7 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { addressTxsQuery.onFilterChange({ filter: newVal }); }, [ addressTxsQuery ]); - const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = (payload) => { + const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = React.useCallback((payload) => { setSocketAlert(''); queryClient.setQueryData( @@ -123,10 +124,10 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { items: [ ...newItems, ...prevData.items, - ], + ].sort(sortTxsFromSocket(sort)), }; }); - }; + }, [ currentAddress, filterValue, overloadCount, queryClient, sort ]); const handleSocketClose = React.useCallback(() => { setSocketAlert('Connection is lost. Please refresh the page to load new transactions.'); diff --git a/ui/txs/sortTxs.test.ts b/ui/txs/sortTxs.test.ts new file mode 100644 index 0000000000..d4d33e5b92 --- /dev/null +++ b/ui/txs/sortTxs.test.ts @@ -0,0 +1,79 @@ +import type { Transaction } from 'types/api/transaction'; + +import sortTxs, { sortTxsFromSocket } from './sortTxs'; + +describe('sortTxs', () => { + it('should sort transactions by value in descending order', () => { + const txs = [ + { value: '42' }, + { value: '11' }, + { value: '24' }, + ] as Array; + const result = txs.sort(sortTxs('value-desc')); + expect(result).toEqual([ + { value: '42' }, + { value: '24' }, + { value: '11' }, + ]); + }); + + it('should sort transactions by value in ascending order', () => { + const txs = [ + { value: '42' }, + { value: '11' }, + { value: '24' }, + ] as Array; + const result = txs.sort(sortTxs('value-asc')); + expect(result).toEqual([ + { value: '11' }, + { value: '24' }, + { value: '42' }, + ]); + }); + + it('should sort transactions by fee in descending order', () => { + const txs = [ + { fee: { value: '42' } }, + { fee: { value: '11' } }, + { fee: { value: '24' } }, + ] as Array; + const result = txs.sort(sortTxs('fee-desc')); + expect(result).toEqual([ + { fee: { value: '42' } }, + { fee: { value: '24' } }, + { fee: { value: '11' } }, + ]); + }); + + it('should sort transactions by fee in ascending order', () => { + const txs = [ + { fee: { value: '42' } }, + { fee: { value: '11' } }, + { fee: { value: '24' } }, + ] as Array; + const result = txs.sort(sortTxs('fee-asc')); + expect(result).toEqual([ + { fee: { value: '11' } }, + { fee: { value: '24' } }, + { fee: { value: '42' } }, + ]); + }); +}); + +describe('sortTxsFromSocket', () => { + it('should sort transaction by age in ascending order if sorting is not provided', () => { + const txs = [ + { timestamp: '2022-11-01T12:33:00Z' }, + { timestamp: '2022-11-01T12:00:00Z' }, + { timestamp: null }, + { timestamp: '2022-11-03T03:03:00Z' }, + ] as Array; + const result = txs.sort(sortTxsFromSocket(undefined)); + expect(result).toEqual([ + { timestamp: null }, + { timestamp: '2022-11-03T03:03:00Z' }, + { timestamp: '2022-11-01T12:33:00Z' }, + { timestamp: '2022-11-01T12:00:00Z' }, + ]); + }); +}); diff --git a/ui/txs/sortTxs.ts b/ui/txs/sortTxs.ts new file mode 100644 index 0000000000..3993612e4c --- /dev/null +++ b/ui/txs/sortTxs.ts @@ -0,0 +1,38 @@ +import type { Transaction, TransactionsSortingValue } from 'types/api/transaction'; + +import compareBns from 'lib/bigint/compareBns'; + +export default function sortTxs(sorting: TransactionsSortingValue | undefined) { + return function sortingFn(tx1: Transaction, tx2: Transaction) { + switch (sorting) { + case 'value-desc': + return compareBns(tx2.value, tx1.value); + case 'value-asc': + return compareBns(tx1.value, tx2.value); + case 'fee-desc': + return compareBns(tx2.fee.value || 0, tx1.fee.value || 0); + case 'fee-asc': + return compareBns(tx1.fee.value || 0, tx2.fee.value || 0); + default: + return 0; + } + }; +} + +export function sortTxsFromSocket(sorting: TransactionsSortingValue | undefined) { + if (sorting) { + return sortTxs(sorting); + } + + return function sortingFn(tx1: Transaction, tx2: Transaction) { + if (!tx1.timestamp) { + return -1; + } + + if (!tx2.timestamp) { + return 1; + } + + return tx2.timestamp.localeCompare(tx1.timestamp); + }; +} diff --git a/ui/txs/useTxsSort.tsx b/ui/txs/useTxsSort.tsx index bee6eefddd..c2ec9d8377 100644 --- a/ui/txs/useTxsSort.tsx +++ b/ui/txs/useTxsSort.tsx @@ -1,13 +1,14 @@ import type { UseQueryResult } from '@tanstack/react-query'; import React from 'react'; -import type { Transaction, TransactionsSortingValue, TxsResponse } from 'types/api/transaction'; +import type { TransactionsSortingValue, TxsResponse } from 'types/api/transaction'; import type { ResourceError } from 'lib/api/resources'; -import compareBns from 'lib/bigint/compareBns'; import * as cookies from 'lib/cookies'; import type { Option } from 'ui/shared/sort/Sort'; +import sortTxs from './sortTxs'; + export const SORT_OPTIONS: Array> = [ { title: 'Default', id: undefined }, { title: 'Value ascending', id: 'value-asc' }, @@ -23,21 +24,6 @@ type HookResult = UseQueryResult> & { setSortByValue: (value: SortingValue) => void; } -const sortTxs = (sorting: SortingValue) => (tx1: Transaction, tx2: Transaction) => { - switch (sorting) { - case 'value-desc': - return compareBns(tx1.value, tx2.value); - case 'value-asc': - return compareBns(tx2.value, tx1.value); - case 'fee-desc': - return compareBns(tx1.fee.value || 0, tx2.fee.value || 0); - case 'fee-asc': - return compareBns(tx2.fee.value || 0, tx1.fee.value || 0); - default: - return 0; - } -}; - export default function useTxsSort( queryResult: UseQueryResult>, ): HookResult { From bfa8a1be0391eef4aa7a42917200171a9ee1fcc0 Mon Sep 17 00:00:00 2001 From: tom Date: Mon, 18 Mar 2024 16:20:36 +0300 Subject: [PATCH 2/2] fix checks workflow --- .github/workflows/checks.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 40ef71d349..9496bb37e6 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -95,6 +95,8 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup node uses: actions/setup-node@v4 @@ -115,7 +117,7 @@ jobs: run: yarn --frozen-lockfile --ignore-optional - name: Run Jest - run: yarn test:jest --onlyChanged=${{ github.event_name == 'pull_request' }} --passWithNoTests + run: yarn test:jest ${{ github.event_name == 'pull_request' && '--changedSince=origin/main' || '' }} --passWithNoTests pw_affected_tests: name: Resolve affected Playwright tests