Skip to content

Commit e6a85f8

Browse files
authored
[etherscan] Fix invalid transfer merge for fee transactions (#949)
1 parent e3c4e11 commit e6a85f8

File tree

3 files changed

+141
-19
lines changed

3 files changed

+141
-19
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Transaction } from '../../../../types/zenmoney'
2+
import { mergeTransferTransactions } from '../../common/converters'
3+
4+
function makeTransaction (
5+
movementId: string,
6+
accountId: string,
7+
sum: number
8+
): Transaction {
9+
return {
10+
hold: null,
11+
date: new Date('2026-01-01T00:00:00.000Z'),
12+
movements: [
13+
{
14+
id: movementId,
15+
account: { id: accountId },
16+
invoice: null,
17+
sum,
18+
fee: 0
19+
}
20+
],
21+
merchant: {
22+
fullTitle: 'merchant',
23+
mcc: null,
24+
location: null
25+
},
26+
comment: null
27+
}
28+
}
29+
30+
describe('mergeTransferTransactions', () => {
31+
it('merges only opposite sign movements with same id', () => {
32+
const result = mergeTransferTransactions([
33+
makeTransaction('hash-1', 'account-1', -100),
34+
makeTransaction('hash-1', 'account-2', 100)
35+
])
36+
37+
expect(result).toHaveLength(1)
38+
expect(result[0].movements).toHaveLength(2)
39+
expect(result[0].merchant).toBe(null)
40+
})
41+
42+
it('does not merge fee-like movements with same sign', () => {
43+
const result = mergeTransferTransactions([
44+
makeTransaction('hash-1_fee', 'account-1', -10),
45+
makeTransaction('hash-1_fee', 'account-2', -10)
46+
])
47+
48+
expect(result).toHaveLength(2)
49+
expect(result[0].movements).toHaveLength(1)
50+
expect(result[1].movements).toHaveLength(1)
51+
})
52+
53+
it('does not merge when one side has zero amount', () => {
54+
const result = mergeTransferTransactions([
55+
makeTransaction('hash-1_fee', 'account-1', -10),
56+
makeTransaction('hash-1_fee', 'account-2', 0)
57+
])
58+
59+
expect(result).toHaveLength(2)
60+
expect(result[0].movements).toHaveLength(1)
61+
expect(result[1].movements).toHaveLength(1)
62+
})
63+
})
Lines changed: 78 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,86 @@
11
import { Transaction } from '../../../types/zenmoney'
22

3+
function canBeMergedAsTransfer (left: Transaction, right: Transaction): boolean {
4+
const leftMovement = left.movements[0]
5+
const rightMovement = right.movements[0]
6+
7+
if (leftMovement == null || rightMovement == null) {
8+
return false
9+
}
10+
11+
if (leftMovement.sum == null || rightMovement.sum == null) {
12+
return false
13+
}
14+
15+
if (leftMovement.sum === 0 || rightMovement.sum === 0) {
16+
return false
17+
}
18+
19+
if (leftMovement.sum < 0) {
20+
return rightMovement.sum > 0
21+
}
22+
23+
return rightMovement.sum < 0
24+
}
25+
326
export function mergeTransferTransactions (transactions: Transaction[]): Transaction[] {
4-
const list = transactions.reduce<{ [key in string]?: Transaction }>((acc, item) => {
5-
const movementId = item.movements[0].id ?? ''
6-
const existingItem = acc[movementId]
7-
8-
if (existingItem == null) {
9-
acc[movementId] = item
10-
} else {
11-
acc[movementId] = {
12-
...existingItem,
13-
movements: [
14-
existingItem.movements[0],
15-
item.movements[0]
16-
],
17-
merchant: null
27+
const result: Transaction[] = []
28+
const usedIndexes = new Set<number>()
29+
30+
for (let i = 0; i < transactions.length; i++) {
31+
if (usedIndexes.has(i)) {
32+
continue
33+
}
34+
35+
const item = transactions[i]
36+
const movement = item.movements[0]
37+
const movementId = movement?.id ?? ''
38+
39+
if (movement == null || movementId === '') {
40+
result.push(item)
41+
continue
42+
}
43+
44+
let pairIndex = -1
45+
for (let j = i + 1; j < transactions.length; j++) {
46+
if (usedIndexes.has(j)) {
47+
continue
48+
}
49+
50+
const candidate = transactions[j]
51+
const candidateMovement = candidate.movements[0]
52+
const candidateMovementId = candidateMovement?.id ?? ''
53+
54+
if (candidateMovement == null || candidateMovementId !== movementId) {
55+
continue
56+
}
57+
58+
if (canBeMergedAsTransfer(item, candidate)) {
59+
pairIndex = j
60+
break
1861
}
1962
}
2063

21-
return acc
22-
}, {})
64+
if (pairIndex === -1) {
65+
result.push(item)
66+
continue
67+
}
68+
69+
const pair = transactions[pairIndex]
70+
const pairMovement = pair.movements[0]
71+
72+
if (pairMovement == null) {
73+
result.push(item)
74+
continue
75+
}
76+
77+
usedIndexes.add(pairIndex)
78+
result.push({
79+
...item,
80+
movements: [movement, pairMovement],
81+
merchant: null
82+
})
83+
}
2384

24-
return Object.values(list) as Transaction[]
85+
return result
2586
}

src/plugins/etherscan/tokens/converters.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,5 @@ export function convertTransactions (
131131
Boolean(transaction?.movements.some((movement) => movement.sum !== 0))
132132
)
133133

134-
console.log('LST', list)
135-
136134
return list
137135
}

0 commit comments

Comments
 (0)