@exodus/activity-txs 6.1.3 → 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,12 @@
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
+
6
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)
7
13
 
8
14
  **Note:** Version bump only for package @exodus/activity-txs
@@ -46,13 +46,14 @@ const createAssetSourceTxsSelector = memoize(
46
46
  const createActivityTxsAtom = ({
47
47
  txLogsAtom,
48
48
  accountStatesAtom,
49
- assetsModule,
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
- walletAccount,
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', 'assetsModule', 'logger', 'errorTracking'],
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",
3
+ "version": "6.1.4",
4
4
  "description": "The activity-txs feature",
5
5
  "author": "Exodus Movement, Inc.",
6
6
  "repository": {
@@ -27,9 +27,10 @@
27
27
  "!**/*.test.js"
28
28
  ],
29
29
  "scripts": {
30
- "lint": "run -T eslint -c ../../eslint.config.mjs",
31
- "lint:fix": "yarn lint --fix",
32
- "test": "run -T exodus-test --jest"
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",
@@ -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.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.5.0",
54
- "@exodus/fiat-ramp": "^15.21.1",
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.1.3",
58
+ "@exodus/orders": "^6.2.2",
58
59
  "@exodus/personal-notes": "^3.9.0",
59
- "@exodus/redux-dependency-injection": "^4.5.0",
60
- "@exodus/wallet-accounts": "^20.4.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": "26c6093fc4c0d572e6575b307f492acab9c978e1"
69
+ "gitHead": "18b49ce03aa47d805068bce4b76bf834be314747"
69
70
  }
@@ -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 groupTxsWithSameOrder = ({ txs, orderSet, isIndexless }) => {
9
- if (!orderSet) return txs.map((tx) => ({ tx, type: 'tx' }))
10
-
11
- const getOrder = (tx) => {
12
- const foundOrder = orderSet.getByTxId(tx.txId)
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
- // If this is a base asset (fee-only tx), don't attach the order
15
- // Base asset is not involved in the swap itself (not fromAsset or toAsset)
16
- if (foundOrder) {
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
- // Base asset transactions should be treated as normal txs, not swaps
21
- if (!isFromAsset && !isToAsset) {
22
- return
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
- const isBatchedTx = tx.coinName === 'bitcoin' && tx.data.sent?.[tx.data.sentIndex]
27
- const isValidTx =
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
- return isValidTx ? foundOrder : undefined
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 = getOrder(tx)
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 { ORDER_STATUS, ORDER_TYPES } from '@exodus/models/lib/fiat-order/constants.js'
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