@exodus/activity-txs 4.1.1 → 4.1.3

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,17 @@
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
+ ## [4.1.3](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/activity-txs@4.1.2...@exodus/activity-txs@4.1.3) (2024-05-27)
7
+
8
+ **Note:** Version bump only for package @exodus/activity-txs
9
+
10
+ ## [4.1.2](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/activity-txs@4.1.1...@exodus/activity-txs@4.1.2) (2024-05-15)
11
+
12
+ ### Bug Fixes
13
+
14
+ - address security feedback ([#6963](https://github.com/ExodusMovement/exodus-hydra/issues/6963)) ([155bdfe](https://github.com/ExodusMovement/exodus-hydra/commit/155bdfed531685108d828be60c9d1a8be9e785ee))
15
+ - indexless order should be ignored in first loop ([#6965](https://github.com/ExodusMovement/exodus-hydra/issues/6965)) ([83766cd](https://github.com/ExodusMovement/exodus-hydra/commit/83766cd69533d81317ea52ab770541a7ccfde274))
16
+
6
17
  ## [4.1.1](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/activity-txs@4.1.0...@exodus/activity-txs@4.1.1) (2024-05-15)
7
18
 
8
19
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/activity-txs",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
4
4
  "description": "The activity-txs feature",
5
5
  "author": "Exodus Movement, Inc.",
6
6
  "repository": {
@@ -36,6 +36,7 @@
36
36
  "@exodus/models": "^10.1.0",
37
37
  "@exodus/module": "^1.2.2",
38
38
  "@exodus/multi-account-redux": "^2.0.1",
39
+ "@exodus/typeforce": "^1.19.0",
39
40
  "lodash": "^4.17.21",
40
41
  "minimalistic-assert": "^1.0.1",
41
42
  "reselect": "^3.0.1"
@@ -44,21 +45,21 @@
44
45
  "@exodus/assets": "^8.0.95",
45
46
  "@exodus/assets-base": "^8.1.10",
46
47
  "@exodus/assets-feature": "^5.3.1",
47
- "@exodus/available-assets": "^8.2.0",
48
+ "@exodus/available-assets": "^8.2.1",
48
49
  "@exodus/bitcoin-plugin": "^1.4.2",
49
50
  "@exodus/blockchain-metadata": "^15.3.1",
50
51
  "@exodus/connected-origins": "^3.3.1",
51
52
  "@exodus/dependency-injection": "^2.2.0",
52
53
  "@exodus/dependency-preprocessors": "^5.2.0",
53
- "@exodus/fiat-ramp": "^10.5.1",
54
- "@exodus/nfts": "^9.2.1",
55
- "@exodus/orders": "^4.8.1",
54
+ "@exodus/fiat-ramp": "^10.6.0",
55
+ "@exodus/nfts": "^9.3.0",
56
+ "@exodus/orders": "^4.9.0",
56
57
  "@exodus/personal-notes": "^3.6.0",
57
58
  "@exodus/redux-dependency-injection": "^3.1.0",
58
59
  "@exodus/storage-memory": "^2.1.1",
59
- "@exodus/wallet-accounts": "^16.5.0",
60
+ "@exodus/wallet-accounts": "^16.5.1",
60
61
  "@types/minimalistic-assert": "^1.0.1",
61
62
  "redux": "^4.2.1"
62
63
  },
63
- "gitHead": "268f3e822c0d1aadc8a42391b9770b435021e230"
64
+ "gitHead": "5f3e666315d60ebef4051ebffa1518a193a7ec43"
64
65
  }
@@ -1,35 +1,40 @@
1
1
  import { createSelector } from 'reselect'
2
2
  import { memoize } from '@exodus/basic-utils'
3
- import { TX_TYPES } from '../constants/tx-types'
4
- import { formatGroupedExchangeTxs, formattersByType } from '../utils/formatters'
3
+ import { formattersByType } from '../utils/activity-formatters'
4
+ import { formatUnindexedOrder } from '../utils/activity-formatters/format-swap-activity'
5
5
 
6
6
  const EMPTY = Object.freeze([])
7
7
 
8
- const generateActivityId = ({ activityItem }) => {
9
- const sentIndex = activityItem.data?.sentIndex
10
- return sentIndex
11
- ? `${activityItem.assetName}.${activityItem.txId}_${sentIndex}`
12
- : `${activityItem.assetName}.${activityItem.txId}`
13
- }
14
-
15
- const groupTxsWithSameOrder = ({ txs, orderSet }) => {
8
+ const groupTxsWithSameOrder = ({ txs, orderSet, isIndexless }) => {
16
9
  const getOrder = (tx) => orderSet.getByTxId(tx.txId)
17
- const exchangeByOrderId = {}
10
+ const exchangeByOrderId = new Map()
18
11
  const result = []
12
+
19
13
  for (const tx of txs) {
20
14
  const order = getOrder(tx)
15
+ if (isIndexless && order) {
16
+ // Orders are the source of truth for indexless swaps.
17
+ // Skip this tx and add it later as exchanged.
18
+ continue
19
+ }
20
+
21
21
  if (order) {
22
- ;(exchangeByOrderId[order.orderId] ?? (exchangeByOrderId[order.orderId] = [])).push(tx)
22
+ if (!exchangeByOrderId.has(order.orderId)) {
23
+ exchangeByOrderId.set(order.orderId, [])
24
+ }
25
+
26
+ exchangeByOrderId.get(order.orderId).push(tx)
23
27
  } else {
24
28
  result.push({ tx, type: 'tx' })
25
29
  }
26
30
  }
27
31
 
28
- Object.values(exchangeByOrderId).forEach((txsGroup) => {
32
+ exchangeByOrderId.forEach((txsGroup) => {
29
33
  const tx = txsGroup.sort((a, b) => b.date.getTime() - a.date.getTime())[0]
30
34
  const order = getOrder(tx)
31
35
  result.push({ tx, type: 'exchange', order })
32
36
  })
37
+
33
38
  return result
34
39
  }
35
40
 
@@ -50,24 +55,22 @@ const createAssetSourceBaseActivitySelectorDefinition = {
50
55
  getPersonalNoteByTxIdSelector,
51
56
  (assets, txs = EMPTY, orderSet, getPersonalNoteByTxId) => {
52
57
  const activityResult = []
53
-
54
- const groupedTxs = groupTxsWithSameOrder({ txs, orderSet })
58
+ const isIndexless = assets[assetName]?.baseAsset?.api?.features?.noHistory
59
+ const groupedTxs = groupTxsWithSameOrder({ txs, orderSet, isIndexless })
55
60
 
56
61
  for (const group of groupedTxs) {
57
62
  const { tx, order, type: formatterType } = group
58
63
  const personalNote = getPersonalNoteByTxId(tx.txId)
59
- const formattedTx = formattersByType[formatterType]({
60
- assets,
64
+ const formattedTx = formattersByType.get(formatterType)({
65
+ asset: assets[tx.coinName],
61
66
  order,
62
67
  tx,
63
68
  personalNote,
64
69
  })
65
- formattedTx.id = generateActivityId({ activityItem: formattedTx })
66
70
 
67
71
  activityResult.push(formattedTx)
68
72
  }
69
73
 
70
- const isIndexless = assets[assetName]?.baseAsset?.api?.features?.noHistory
71
74
  if (isIndexless) {
72
75
  const orderIdsFromActivity = new Set(activityResult.map((tx) => tx.orderId))
73
76
  const ordersForIndexlessAssets = [...orderSet].filter(
@@ -80,30 +83,8 @@ const createAssetSourceBaseActivitySelectorDefinition = {
80
83
  // Prevent dups by checking if this order already exists.
81
84
  if (orderIdsFromActivity.has(order.orderId)) return
82
85
 
83
- const isFromIndexlessAsset = order.fromAsset === assetName
84
- const formattedItem = formatGroupedExchangeTxs({
85
- tx: {
86
- type: TX_TYPES.EXCHANGE,
87
- date: order.date,
88
- order,
89
- pending: !['failed-final', 'complete-verified'].includes(order.status),
90
- failed: order.status === 'failed-final',
91
- data: { swapTx: true },
92
- selfSend: false,
93
- token: null, // TBD
94
- bumpType: {},
95
- coinName: isFromIndexlessAsset ? order.fromAsset : order.toAsset,
96
- value: isFromIndexlessAsset ? order.fromAmount : order.toAmount,
97
- coinAmount: isFromIndexlessAsset ? order.fromAmount : order.toAmount,
98
- txId: isFromIndexlessAsset ? order.fromTxId : order.toTxId,
99
- sent: isFromIndexlessAsset,
100
- received: !isFromIndexlessAsset,
101
- },
102
- assets,
103
- order,
104
- })
105
-
106
- formattedItem.id = generateActivityId({ activityItem: formattedItem })
86
+ const formattedItem = formatUnindexedOrder({ assetName, assets, order })
87
+
107
88
  activityResult.push(formattedItem)
108
89
  })
109
90
  }
@@ -6,11 +6,14 @@ const selectorFactory =
6
6
  createWithConnectionsActivity
7
7
  ) =>
8
8
  // call this once on client-side
9
- (options = {}) => {
9
+ (options = Object.create(null)) => {
10
10
  let createFullSelector = createAssetSourceBaseActivity
11
11
  createFullSelector = createWithNftsActivity({
12
12
  createActivitySelector: createFullSelector,
13
- nftsNetworkNameToAssetName: options.nftsNetworkNameToAssetName || {},
13
+ nftsNetworkNameToAssetName:
14
+ (Object.hasOwn(options, 'nftsNetworkNameToAssetName') &&
15
+ options.nftsNetworkNameToAssetName) ||
16
+ Object.create(null),
14
17
  })
15
18
  createFullSelector = createWithFiatActivity({ createActivitySelector: createFullSelector })
16
19
  createFullSelector = createWithConnectionsActivity({
@@ -1,8 +1,7 @@
1
1
  import { memoize } from '@exodus/basic-utils'
2
2
  import { createSelector } from 'reselect'
3
- import { formatUnindexedNftTx } from '../utils/formatters'
4
3
  import assert from 'minimalistic-assert'
5
- import { TX_TYPES } from '../constants/tx-types'
4
+ import { formatNftTx, formatUnindexedNftTx } from '../utils/activity-formatters/format-nft'
6
5
 
7
6
  const selectorFactory =
8
7
  (createAssetSourceNftTxsById, assetsSelector) =>
@@ -33,12 +32,9 @@ const selectorFactory =
33
32
  if (nft) {
34
33
  hasChanges = true
35
34
  indexedNftTxIds.add(nft.txId)
36
- resultActivity.push({
37
- ...activityItem,
38
- nft,
39
- type: TX_TYPES.NFT,
40
- sent: nft.sent === undefined ? activityItem.sent : nft.sent, // rely on nft metadata for nft txs
41
- })
35
+ resultActivity.push(
36
+ formatNftTx({ ...activityItem, nft, assetName: activityItem.assetName })
37
+ )
42
38
  } else {
43
39
  resultActivity.push(activityItem)
44
40
  }
@@ -47,6 +43,7 @@ const selectorFactory =
47
43
  ;[...nftsByTxId.values()]
48
44
  .filter(
49
45
  (nftTx) =>
46
+ Object.hasOwn(nftsNetworkNameToAssetName, nftTx.network) &&
50
47
  assetName === nftsNetworkNameToAssetName[nftTx.network] &&
51
48
  !indexedNftTxIds.has(nftTx.txId)
52
49
  )
@@ -54,7 +51,7 @@ const selectorFactory =
54
51
  hasChanges = true
55
52
  const assetName = nftsNetworkNameToAssetName[nftTx.network]
56
53
  const asset = assets[assetName]
57
- const formatted = formatUnindexedNftTx({ nftTx, asset })
54
+ const formatted = formatUnindexedNftTx({ nftTx, assetName: asset.name })
58
55
 
59
56
  resultActivity.push(formatted)
60
57
  })
@@ -0,0 +1,38 @@
1
+ import { getSharedProps } from './get-shared-props'
2
+ import { TX_TYPES } from '../../constants/tx-types'
3
+
4
+ export const formatUnindexedNftTx = ({ assetName, nftTx }) => {
5
+ return {
6
+ ...getSharedProps({
7
+ txId: nftTx.txId,
8
+ date: new Date(nftTx.date),
9
+ assetName,
10
+ type: TX_TYPES.NFT,
11
+ id: `nft_${assetName}.${nftTx.txId}`,
12
+ pending: false,
13
+ failed: false,
14
+ sent: nftTx.sent,
15
+ received: !nftTx.sent,
16
+ }),
17
+ nft: nftTx,
18
+ }
19
+ }
20
+
21
+ export const formatNftTx = ({ tx, nft, assetName, ...rest }) => {
22
+ return {
23
+ ...rest,
24
+ ...getSharedProps({
25
+ txId: nft.txId,
26
+ date: tx.date,
27
+ assetName,
28
+ type: TX_TYPES.NFT,
29
+ id: `nft_${assetName}.${tx.txId}`,
30
+ pending: false,
31
+ failed: tx.failed,
32
+ sent: nft.sent,
33
+ received: !nft.sent,
34
+ }),
35
+ nft,
36
+ tx,
37
+ }
38
+ }
@@ -0,0 +1,93 @@
1
+ import { getSharedProps } from './get-shared-props'
2
+ import { TX_TYPES } from '../../constants/tx-types'
3
+
4
+ const FINAL_ORDER_STATUS = {
5
+ inProgress: 'in-progress',
6
+ failed: 'failed',
7
+ success: 'complete',
8
+ }
9
+
10
+ const EXODEX_TX_TYPES = {
11
+ PRE_SETUP: 'preSetupTransaction',
12
+ SETUP: 'setupTransaction',
13
+ SWAP: 'swapTransaction',
14
+ CLEANUP: 'cleanupTransaction',
15
+ }
16
+
17
+ const UI_TX_TYPE_FROM_EXODEX = new Map([
18
+ [EXODEX_TX_TYPES.PRE_SETUP, TX_TYPES.APPROVAL],
19
+ [EXODEX_TX_TYPES.SETUP, TX_TYPES.APPROVAL],
20
+ [EXODEX_TX_TYPES.SWAP, TX_TYPES.EXCHANGE],
21
+ [EXODEX_TX_TYPES.CLEANUP, TX_TYPES.CLEANUP],
22
+ ])
23
+
24
+ function getOrderType({ order, tx }) {
25
+ const txInfoFromOrder = tx
26
+ ? order.txIds?.find((txFromOrder) => txFromOrder.txId === tx.txId)
27
+ : null
28
+ return UI_TX_TYPE_FROM_EXODEX.get(txInfoFromOrder?.txType) || TX_TYPES.EXCHANGE
29
+ }
30
+
31
+ const getSwapSharedProps = ({
32
+ date,
33
+ assetName,
34
+ txId,
35
+ tx,
36
+ order,
37
+ failed,
38
+ displayAmount,
39
+ personalNote,
40
+ pending,
41
+ }) => {
42
+ return {
43
+ ...getSharedProps({
44
+ txId,
45
+ date,
46
+ id: `swap_${assetName}.${txId}`,
47
+ assetName,
48
+ type: getOrderType({ tx, order }),
49
+ pending,
50
+ failed,
51
+ sent: order.fromAsset === assetName,
52
+ received: order.toAsset === assetName,
53
+ }),
54
+ tx,
55
+ fromAsset: order.fromAsset,
56
+ toAsset: order.toAsset,
57
+ orderId: order.orderId,
58
+ displayAmount,
59
+ personalNote,
60
+ }
61
+ }
62
+
63
+ export const formatSwapActivity = ({ tx, order, personalNote }) =>
64
+ getSwapSharedProps({
65
+ date: tx.date,
66
+ assetName: order.toAsset,
67
+ txId: tx.txId,
68
+ tx,
69
+ order,
70
+ failed: tx.failed || order.error,
71
+ displayAmount: order.toAmount.abs(),
72
+ personalNote,
73
+ pending: order.exodusStatus === FINAL_ORDER_STATUS.inProgress,
74
+ })
75
+
76
+ export const formatUnindexedOrder = ({ order, assetName: _assetName, personalNote }) => {
77
+ const isFromIndexlessAsset = order.fromAsset === _assetName
78
+ const assetName = isFromIndexlessAsset ? order.fromAsset : order.toAsset
79
+ let displayAmount = isFromIndexlessAsset ? order.fromAmount : order.toAmount
80
+ displayAmount = displayAmount.abs()
81
+ const txId = isFromIndexlessAsset ? order.fromTxId : order.toTxId
82
+
83
+ return getSwapSharedProps({
84
+ date: order.date,
85
+ assetName,
86
+ txId,
87
+ order,
88
+ failed: order.error,
89
+ displayAmount,
90
+ personalNote,
91
+ pending: order.exodusStatus === FINAL_ORDER_STATUS.inProgress,
92
+ })
93
+ }
@@ -0,0 +1,39 @@
1
+ import { TX_TYPES } from '../../constants/tx-types'
2
+ import { getSharedProps } from './get-shared-props'
3
+
4
+ function getTxType({ tx }) {
5
+ return tx.sent ? TX_TYPES.SENT : TX_TYPES.RECEIVED
6
+ }
7
+
8
+ const getMinConfirmations = (asset) => asset.baseAsset.minConfirmations || 1
9
+
10
+ export function isPending(tx, asset) {
11
+ return (
12
+ tx.confirmations < getMinConfirmations(asset) &&
13
+ !tx.dropped && // dropped txs are not pending anymore
14
+ !tx.error // errored txs are not pending anymore
15
+ )
16
+ }
17
+
18
+ const generateTxActivityId = ({ tx, asset }) => {
19
+ const sentIndex = tx.data?.sentIndex
20
+ return sentIndex ? `${asset.name}.${tx.txId}_${sentIndex}` : `${asset.name}.${tx.txId}`
21
+ }
22
+
23
+ export const formatTxActivity = ({ tx, asset, personalNote }) => {
24
+ return {
25
+ ...getSharedProps({
26
+ txId: tx.txId,
27
+ date: tx.date,
28
+ id: generateTxActivityId({ tx, asset }),
29
+ assetName: asset.name,
30
+ type: getTxType({ tx }),
31
+ pending: isPending(tx, asset),
32
+ failed: tx.failed,
33
+ sent: tx.sent,
34
+ received: tx.received,
35
+ }),
36
+ tx,
37
+ personalNote,
38
+ }
39
+ }
@@ -0,0 +1,25 @@
1
+ import typeforce from '@exodus/typeforce'
2
+
3
+ export const getSharedProps = (opts) => {
4
+ try {
5
+ typeforce(
6
+ opts,
7
+ {
8
+ txId: 'String',
9
+ date: 'Date',
10
+ id: 'String',
11
+ assetName: 'String', // used to filter by asset on client side
12
+ type: 'String',
13
+ pending: 'Boolean', // tx uses tx.pending, order checks order status
14
+ failed: 'Boolean', // tx uses tx.failed, order uses status
15
+ sent: 'Boolean', // tx.uses tx.sent, nft uses nft.sent
16
+ received: 'Boolean', // tx.uses tx.receive, nft uses !nft.sent
17
+ },
18
+ true // no extra props allowed
19
+ )
20
+ } catch {
21
+ console.warn('activity item props dont pass validation', opts)
22
+ }
23
+
24
+ return opts
25
+ }
@@ -0,0 +1,7 @@
1
+ import { formatSwapActivity } from './format-swap-activity'
2
+ import { formatTxActivity } from './format-tx-activity'
3
+
4
+ export const formattersByType = new Map([
5
+ ['exchange', formatSwapActivity],
6
+ ['tx', formatTxActivity],
7
+ ])
@@ -1,107 +0,0 @@
1
- import { TX_TYPES } from '../constants/tx-types'
2
-
3
- const FINAL_ORDER_STATUS = {
4
- inProgress: 'in-progress',
5
- failed: 'failed',
6
- success: 'complete',
7
- }
8
-
9
- const EXODEX_TX_TYPES = {
10
- PRE_SETUP: 'preSetupTransaction',
11
- SETUP: 'setupTransaction',
12
- SWAP: 'swapTransaction',
13
- CLEANUP: 'cleanupTransaction',
14
- }
15
-
16
- const UI_TX_TYPE_FROM_EXODEX = {
17
- [EXODEX_TX_TYPES.PRE_SETUP]: TX_TYPES.APPROVAL,
18
- [EXODEX_TX_TYPES.SETUP]: TX_TYPES.APPROVAL,
19
- [EXODEX_TX_TYPES.SWAP]: TX_TYPES.EXCHANGE,
20
- [EXODEX_TX_TYPES.CLEANUP]: TX_TYPES.CLEANUP,
21
- }
22
-
23
- function getTxType({ tx, order }) {
24
- if (order) {
25
- const txInfoFromOrder = order.txIds?.find((txFromOrder) => txFromOrder.txId === tx.txId)
26
- return UI_TX_TYPE_FROM_EXODEX[txInfoFromOrder?.txType] || TX_TYPES.EXCHANGE
27
- }
28
-
29
- return tx.sent ? TX_TYPES.SENT : TX_TYPES.RECEIVED
30
- }
31
-
32
- const getMinConfirmations = (asset) => asset.baseAsset.minConfirmations || 1
33
-
34
- export function isPending(tx, asset) {
35
- return (
36
- tx.confirmations < getMinConfirmations(asset) &&
37
- !tx.dropped && // dropped txs are not pending anymore
38
- !tx.error // errored txs are not pending anymore
39
- )
40
- }
41
-
42
- const formatBase = ({ tx, asset, order, personalNote }) => ({
43
- ...tx,
44
- assetName: asset.name,
45
- displayTicker: asset.displayTicker,
46
- type: getTxType({ tx, asset, order }),
47
- time: tx.date,
48
- amount: tx.coinAmount.abs().toDefaultNumber(),
49
- pending: isPending(tx, asset),
50
- isOrderInProgress: !!order && order.exodusStatus === FINAL_ORDER_STATUS.inProgress,
51
- confirmations: tx.confirmations,
52
- failed: tx.failed,
53
- received: tx.received,
54
- personalNote,
55
- })
56
-
57
- export const formatGroupedExchangeTxs = ({ tx, assets, order, personalNote }) => {
58
- const toAsset = assets[order.toAsset]
59
-
60
- return {
61
- ...formatBase({ tx, asset: toAsset, order, personalNote }),
62
- failed: tx.failed || order.error,
63
- pending: order.exodusStatus === FINAL_ORDER_STATUS.inProgress,
64
- assetName: toAsset.name,
65
- txId: tx.txId,
66
- type: TX_TYPES.EXCHANGE,
67
- time: tx.date,
68
- fromAsset: order.fromAsset,
69
- toAsset: order.toAsset,
70
- amount: order.toAmount,
71
- orderId: order.orderId,
72
- data: {
73
- swapTx: true,
74
- },
75
- }
76
- }
77
-
78
- const formatGroupedTx = ({ tx, assets, personalNote }) =>
79
- formatBase({
80
- tx,
81
- asset: assets[tx.coinName],
82
- personalNote,
83
- })
84
-
85
- export const formattersByType = {
86
- exchange: formatGroupedExchangeTxs,
87
- tx: formatGroupedTx,
88
- }
89
-
90
- export const formatUnindexedNftTx = ({ asset, nftTx }) => {
91
- return {
92
- id: `${asset.name}.${nftTx.txId}`,
93
- coinName: asset.name,
94
- assetName: asset.name,
95
- displayTicker: asset.displayTicker,
96
- type: TX_TYPES.NFT,
97
- time: new Date(nftTx.date),
98
- from: nftTx.from,
99
- to: nftTx.to,
100
- sent: Boolean(nftTx.sent),
101
- received: !nftTx.sent,
102
- date: new Date(nftTx.date),
103
- nft: nftTx,
104
- txId: nftTx.txId,
105
- selfSend: false,
106
- }
107
- }