@exodus/activity-txs 6.1.2 → 6.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [6.1.4](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/activity-txs@6.1.3...@exodus/activity-txs@6.1.4) (2026-06-24)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **activity-txs:** collapse chained swap bridge legs into one row ([#17326](https://github.com/ExodusMovement/exodus-hydra/issues/17326)) ([befd253](https://github.com/ExodusMovement/exodus-hydra/commit/befd253a6853bcc15ddf64ba8212adde1ad53c33))
|
|
11
|
+
|
|
12
|
+
## [6.1.3](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/activity-txs@6.1.2...@exodus/activity-txs@6.1.3) (2026-05-13)
|
|
13
|
+
|
|
14
|
+
**Note:** Version bump only for package @exodus/activity-txs
|
|
15
|
+
|
|
6
16
|
## [6.1.2](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/activity-txs@6.1.1...@exodus/activity-txs@6.1.2) (2026-05-08)
|
|
7
17
|
|
|
8
18
|
### Bug Fixes
|
package/atoms/activity-txs.js
CHANGED
|
@@ -46,13 +46,14 @@ const createAssetSourceTxsSelector = memoize(
|
|
|
46
46
|
const createActivityTxsAtom = ({
|
|
47
47
|
txLogsAtom,
|
|
48
48
|
accountStatesAtom,
|
|
49
|
-
|
|
49
|
+
assetsAtom,
|
|
50
50
|
logger,
|
|
51
51
|
errorTracking,
|
|
52
52
|
}) => {
|
|
53
53
|
const inputAtom = combine({
|
|
54
54
|
txLogs: txLogsAtom,
|
|
55
55
|
accountStates: accountStatesAtom,
|
|
56
|
+
assetsAtomValue: assetsAtom,
|
|
56
57
|
})
|
|
57
58
|
|
|
58
59
|
let prevResult = Object.create(null)
|
|
@@ -70,16 +71,10 @@ const createActivityTxsAtom = ({
|
|
|
70
71
|
return walletCache
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
function processActivityForAsset(
|
|
74
|
-
|
|
75
|
-
assetName,
|
|
76
|
-
assetTxSet,
|
|
77
|
-
accountStatesValue,
|
|
78
|
-
runId
|
|
79
|
-
) {
|
|
74
|
+
function processActivityForAsset(walletAccount, asset, assetTxSet, accountStatesValue, runId) {
|
|
75
|
+
const assetName = asset.name
|
|
80
76
|
const walletCache = getWalletCache(walletAccount)
|
|
81
77
|
const cached = walletCache.get(assetName)
|
|
82
|
-
const asset = assetsModule.getAsset(assetName)
|
|
83
78
|
const baseAssetName = asset.baseAsset.name
|
|
84
79
|
const baseAssetAccountState = accountStatesValue?.[walletAccount]?.[baseAssetName]
|
|
85
80
|
|
|
@@ -171,10 +166,11 @@ const createActivityTxsAtom = ({
|
|
|
171
166
|
return txs
|
|
172
167
|
}
|
|
173
168
|
|
|
174
|
-
const selector = ({ txLogs, accountStates }) => {
|
|
169
|
+
const selector = ({ txLogs, accountStates, assetsAtomValue }) => {
|
|
175
170
|
const runId = (runIdCounter += 1)
|
|
176
171
|
const txLogsValue = txLogs?.value ?? Object.create(null)
|
|
177
172
|
const accountStatesValue = accountStates?.value ?? Object.create(null)
|
|
173
|
+
const assets = assetsAtomValue?.value ?? Object.create(null)
|
|
178
174
|
|
|
179
175
|
let activeCount = 0
|
|
180
176
|
|
|
@@ -199,13 +195,14 @@ const createActivityTxsAtom = ({
|
|
|
199
195
|
|
|
200
196
|
if (prevWallet) {
|
|
201
197
|
for (const assetName in accountTxLogs) {
|
|
198
|
+
if (!Object.prototype.hasOwnProperty.call(assets, assetName)) continue
|
|
202
199
|
if (!Object.prototype.hasOwnProperty.call(accountTxLogs, assetName)) continue
|
|
203
200
|
assetsCount += 1
|
|
204
201
|
activeCount += 1
|
|
205
202
|
|
|
206
203
|
const activityTxs = processActivityForAsset(
|
|
207
204
|
walletAccount,
|
|
208
|
-
assetName,
|
|
205
|
+
assets[assetName],
|
|
209
206
|
accountTxLogs[assetName],
|
|
210
207
|
accountStatesValue,
|
|
211
208
|
runId
|
|
@@ -236,12 +233,13 @@ const createActivityTxsAtom = ({
|
|
|
236
233
|
|
|
237
234
|
const nextWallet = Object.create(null)
|
|
238
235
|
for (const assetName in accountTxLogs) {
|
|
236
|
+
if (!Object.prototype.hasOwnProperty.call(assets, assetName)) continue
|
|
239
237
|
if (!Object.prototype.hasOwnProperty.call(accountTxLogs, assetName)) continue
|
|
240
238
|
activeCount += 1
|
|
241
239
|
|
|
242
240
|
const activityTxs = processActivityForAsset(
|
|
243
241
|
walletAccount,
|
|
244
|
-
assetName,
|
|
242
|
+
assets[assetName],
|
|
245
243
|
accountTxLogs[assetName],
|
|
246
244
|
accountStatesValue,
|
|
247
245
|
runId
|
|
@@ -287,7 +285,7 @@ const activityTxsAtomDefinition = {
|
|
|
287
285
|
id: 'activityTxsAtom',
|
|
288
286
|
type: 'atom',
|
|
289
287
|
factory: createActivityTxsAtom,
|
|
290
|
-
dependencies: ['txLogsAtom', 'accountStatesAtom', '
|
|
288
|
+
dependencies: ['txLogsAtom', 'accountStatesAtom', 'assetsAtom', 'logger', 'errorTracking'],
|
|
291
289
|
public: true,
|
|
292
290
|
}
|
|
293
291
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/activity-txs",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.4",
|
|
4
4
|
"description": "The activity-txs feature",
|
|
5
5
|
"author": "Exodus Movement, Inc.",
|
|
6
6
|
"repository": {
|
|
@@ -27,16 +27,17 @@
|
|
|
27
27
|
"!**/*.test.js"
|
|
28
28
|
],
|
|
29
29
|
"scripts": {
|
|
30
|
-
"lint": "
|
|
31
|
-
"lint:fix": "
|
|
32
|
-
"test": "
|
|
30
|
+
"lint": "pnpm exec eslint -c ../../eslint.config.mjs",
|
|
31
|
+
"lint:fix": "pnpm run lint --fix",
|
|
32
|
+
"test": "pnpm exec exodus-test --jest",
|
|
33
|
+
"typecheck": "pnpm exec tsc -p tsconfig.json --noEmit"
|
|
33
34
|
},
|
|
34
35
|
"dependencies": {
|
|
35
36
|
"@exodus/atoms": "^10.3.3",
|
|
36
37
|
"@exodus/basic-utils": "^5.0.0",
|
|
37
38
|
"@exodus/core-selectors": "^4.0.0",
|
|
38
39
|
"@exodus/models": "^13.0.0",
|
|
39
|
-
"@exodus/multi-account-redux": "^
|
|
40
|
+
"@exodus/multi-account-redux": "^3.0.0",
|
|
40
41
|
"@exodus/safe-string": "^1.3.0",
|
|
41
42
|
"@exodus/typeforce": "^1.19.0",
|
|
42
43
|
"lodash": "^4.17.21",
|
|
@@ -46,18 +47,18 @@
|
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@exodus/assets": "^11.0.0",
|
|
48
49
|
"@exodus/assets-base": "^12.0.0",
|
|
49
|
-
"@exodus/assets-feature": "^9.2.
|
|
50
|
+
"@exodus/assets-feature": "^9.2.3",
|
|
50
51
|
"@exodus/available-assets": "^8.11.0",
|
|
51
52
|
"@exodus/bitcoin-plugin": "^2.0.0",
|
|
52
53
|
"@exodus/connected-origins": "^4.3.0",
|
|
53
|
-
"@exodus/error-tracking": "^3.
|
|
54
|
-
"@exodus/fiat-ramp": "^15.21.
|
|
54
|
+
"@exodus/error-tracking": "^3.6.0",
|
|
55
|
+
"@exodus/fiat-ramp": "^15.21.2",
|
|
55
56
|
"@exodus/logger": "^1.2.3",
|
|
56
57
|
"@exodus/nfts": "^9.6.2",
|
|
57
|
-
"@exodus/orders": "^6.
|
|
58
|
+
"@exodus/orders": "^6.2.2",
|
|
58
59
|
"@exodus/personal-notes": "^3.9.0",
|
|
59
|
-
"@exodus/redux-dependency-injection": "^
|
|
60
|
-
"@exodus/wallet-accounts": "^20.4.
|
|
60
|
+
"@exodus/redux-dependency-injection": "^5.0.0",
|
|
61
|
+
"@exodus/wallet-accounts": "^20.4.7",
|
|
61
62
|
"@types/minimalistic-assert": "^1.0.1",
|
|
62
63
|
"redux": "^4.2.1"
|
|
63
64
|
},
|
|
@@ -65,5 +66,5 @@
|
|
|
65
66
|
"access": "public",
|
|
66
67
|
"provenance": false
|
|
67
68
|
},
|
|
68
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "18b49ce03aa47d805068bce4b76bf834be314747"
|
|
69
70
|
}
|
package/redux/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import id from './id.js'
|
|
|
2
2
|
import initialState from './initial-state.js'
|
|
3
3
|
import selectorDefinitions from './selectors/index.js'
|
|
4
4
|
import helper from './multi-account-helper.js'
|
|
5
|
-
import { setAccounts } from '@exodus/multi-account-redux
|
|
5
|
+
import { setAccounts } from '@exodus/multi-account-redux'
|
|
6
6
|
|
|
7
7
|
const activityTxsReduxDefinition = {
|
|
8
8
|
id,
|
|
@@ -5,38 +5,69 @@ import { formatUnindexedOrder } from '../utils/activity-formatters/format-swap-a
|
|
|
5
5
|
|
|
6
6
|
const EMPTY = Object.freeze([])
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
const CHAINED_SVC = 'chained'
|
|
9
|
+
|
|
10
|
+
// Outcomes of classifying a tx against the order set:
|
|
11
|
+
// - 'exchange': tx belongs to a swap and should be grouped into an exchange row
|
|
12
|
+
// - 'tx': tx is a plain send/receive (or a base-asset fee tx) and shows on its own
|
|
13
|
+
// - 'suppress': tx is the intermediate (bridge) leg of a chained swap and is hidden
|
|
14
|
+
const classifyTx = ({ tx, orderSet }) => {
|
|
15
|
+
const foundOrder = orderSet.getByTxId(tx.txId)
|
|
16
|
+
if (!foundOrder) return { type: 'tx' }
|
|
17
|
+
|
|
18
|
+
const isFromAsset = tx.coinName === foundOrder.fromAsset
|
|
19
|
+
const isToAsset = tx.coinName === foundOrder.toAsset
|
|
20
|
+
|
|
21
|
+
// The tx is linked to an order but moves an asset that is neither the
|
|
22
|
+
// swap's source nor destination.
|
|
23
|
+
if (!isFromAsset && !isToAsset) {
|
|
24
|
+
// Chained swaps (e.g. XO -> USDC -> Ondo) route through a bridge asset that
|
|
25
|
+
// briefly lands in the user's wallet on the same on-chain txs as the first
|
|
26
|
+
// and last legs. Those bridge-side rows (e.g. USDC received then sent) are
|
|
27
|
+
// noise: the swap already appears end-to-end as fromAsset -> toAsset. Hide
|
|
28
|
+
// them so users don't think they received an unrelated asset.
|
|
29
|
+
//
|
|
30
|
+
// Only suppress an actual bridge *movement* (non-zero principal). A
|
|
31
|
+
// base-asset fee-only tx (e.g. ETH/SOL gas debit) is also "neither from nor
|
|
32
|
+
// to asset", but it is a real one-way balance change, so it must stay
|
|
33
|
+
// visible — hiding it would drop the base-asset balance with no activity row.
|
|
34
|
+
const isBridgeMovement = Boolean(tx.coinAmount) && !tx.coinAmount.isZero
|
|
35
|
+
|
|
36
|
+
// Only hide bridge legs once the chained swap has completed. The "noise"
|
|
37
|
+
// justification relies on the bridge being a round-trip that nets to zero
|
|
38
|
+
// (received on leg 1, sent on leg 2). While the order is in progress or has
|
|
39
|
+
// failed/refunded, the user may genuinely be holding the bridge asset, so
|
|
40
|
+
// the movement must stay visible — otherwise the balance changes with no
|
|
41
|
+
// activity row explaining where the asset came from.
|
|
42
|
+
const isCompleted = foundOrder.exodusStatus === 'complete'
|
|
43
|
+
if (foundOrder.svc === CHAINED_SVC && isBridgeMovement && isCompleted) {
|
|
44
|
+
return { type: 'suppress' }
|
|
45
|
+
}
|
|
13
46
|
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const isFromAsset = tx.coinName === foundOrder.fromAsset
|
|
18
|
-
const isToAsset = tx.coinName === foundOrder.toAsset
|
|
47
|
+
// Otherwise this is a base-asset (fee-only) tx, shown as a normal tx.
|
|
48
|
+
return { type: 'tx' }
|
|
49
|
+
}
|
|
19
50
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
}
|
|
51
|
+
const isBatchedTx = tx.coinName === 'bitcoin' && tx.data.sent?.[tx.data.sentIndex]
|
|
52
|
+
const isValidTx =
|
|
53
|
+
isBatchedTx && foundOrder?.fromAmount ? tx.coinAmount.abs().equals(foundOrder.fromAmount) : true
|
|
25
54
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
isBatchedTx && foundOrder?.fromAmount
|
|
29
|
-
? tx.coinAmount.abs().equals(foundOrder.fromAmount)
|
|
30
|
-
: true
|
|
55
|
+
return isValidTx ? { type: 'exchange', order: foundOrder } : { type: 'tx' }
|
|
56
|
+
}
|
|
31
57
|
|
|
32
|
-
|
|
33
|
-
}
|
|
58
|
+
const groupTxsWithSameOrder = ({ txs, orderSet, isIndexless }) => {
|
|
59
|
+
if (!orderSet) return txs.map((tx) => ({ tx, type: 'tx' }))
|
|
34
60
|
|
|
35
61
|
const exchangeByOrderId = new Map()
|
|
36
62
|
const result = []
|
|
37
63
|
|
|
38
64
|
for (const tx of txs) {
|
|
39
|
-
const order =
|
|
65
|
+
const { type, order } = classifyTx({ tx, orderSet })
|
|
66
|
+
|
|
67
|
+
if (type === 'suppress') {
|
|
68
|
+
// Bridge leg of a chained swap, intentionally omitted from activity.
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
40
71
|
|
|
41
72
|
if (isIndexless && order) {
|
|
42
73
|
// Orders are the source of truth for indexless swaps.
|
|
@@ -46,18 +77,17 @@ const groupTxsWithSameOrder = ({ txs, orderSet, isIndexless }) => {
|
|
|
46
77
|
|
|
47
78
|
if (order) {
|
|
48
79
|
if (!exchangeByOrderId.has(order.orderId)) {
|
|
49
|
-
exchangeByOrderId.set(order.orderId, [])
|
|
80
|
+
exchangeByOrderId.set(order.orderId, { order, txs: [] })
|
|
50
81
|
}
|
|
51
82
|
|
|
52
|
-
exchangeByOrderId.get(order.orderId).push(tx)
|
|
83
|
+
exchangeByOrderId.get(order.orderId).txs.push(tx)
|
|
53
84
|
} else {
|
|
54
85
|
result.push({ tx, type: 'tx' })
|
|
55
86
|
}
|
|
56
87
|
}
|
|
57
88
|
|
|
58
|
-
exchangeByOrderId.forEach((txsGroup) => {
|
|
89
|
+
exchangeByOrderId.forEach(({ order, txs: txsGroup }) => {
|
|
59
90
|
const tx = txsGroup.sort((a, b) => b.date.getTime() - a.date.getTime())[0]
|
|
60
|
-
const order = getOrder(tx)
|
|
61
91
|
result.push({ tx, type: 'exchange', order })
|
|
62
92
|
})
|
|
63
93
|
|
|
@@ -2,7 +2,9 @@ import { memoize } from '@exodus/basic-utils'
|
|
|
2
2
|
import { createSelector } from 'reselect'
|
|
3
3
|
import assert from 'minimalistic-assert'
|
|
4
4
|
import { TX_TYPES } from '../constants/tx-types.js'
|
|
5
|
-
import {
|
|
5
|
+
import { FIAT_ORDER } from '@exodus/models'
|
|
6
|
+
|
|
7
|
+
const { ORDER_STATUS, ORDER_TYPES } = FIAT_ORDER
|
|
6
8
|
|
|
7
9
|
const ORDER_TX_MATCH_WINDOW_MS = 3 * 24 * 60 * 60 * 1000
|
|
8
10
|
|