@exodus/ethereum-api 8.9.0 → 8.9.2

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,26 @@
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
+ ## [8.9.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.9.1...@exodus/ethereum-api@8.9.2) (2024-07-03)
7
+
8
+ ### Code Refactoring
9
+
10
+ * **ethereum:** EVM balances ([#2598](https://github.com/ExodusMovement/assets/issues/2598)) ([fb6b937](https://github.com/ExodusMovement/assets/commit/fb6b9375675494592b42a9c7ce1ccfa505bc8946))
11
+
12
+
13
+ ## [8.9.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.9.0...@exodus/ethereum-api@8.9.1) (2024-07-03)
14
+
15
+ ### Features
16
+
17
+ * patterns for filtering sensitive hint in EthLikeError ([#2721](https://github.com/ExodusMovement/assets/issues/2721)) ([7de766c](https://github.com/ExodusMovement/assets/commit/7de766c20a85168bb11117234ceab4475b26af10))
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * update SOL staking info on balance change ([#2672](https://github.com/ExodusMovement/assets/issues/2672)) ([bc2043c](https://github.com/ExodusMovement/assets/commit/bc2043ce226128d3e321937a325e20382f12f874))
23
+
24
+
25
+
6
26
  ## [8.9.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.8.0...@exodus/ethereum-api@8.9.0) (2024-07-03)
7
27
 
8
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.9.0",
3
+ "version": "8.9.2",
4
4
  "description": "Ethereum Api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -66,5 +66,5 @@
66
66
  "type": "git",
67
67
  "url": "git+https://github.com/ExodusMovement/assets.git"
68
68
  },
69
- "gitHead": "237c799e9bc8df6574be95056a999896e2503290"
69
+ "gitHead": "aeda2367cf089f8ac0c1a98fc104d04d995328b9"
70
70
  }
@@ -10,7 +10,7 @@ export const reasons = {
10
10
  }
11
11
 
12
12
  const MAX_HINT_LENGTH = 100
13
- // TODO: move this to be an generic error for all assets
13
+ // TODO: move this to be a generic error for all assets
14
14
  export class EthLikeError extends Error {
15
15
  #hintStack
16
16
 
@@ -18,7 +18,6 @@ export class EthLikeError extends Error {
18
18
  * Creates an instance of EthLikeError.
19
19
  *
20
20
  * @param {string} message - Standard error message.
21
- * @param {string} tag - Tag associated with the error.
22
21
  * @param {string} reason - A constant indicating the generic failure. Must not contain any sensitive information such as private keys, transaction IDs, or wallet addresses.
23
22
  * @param {string} hint - A hint to help the user understand the error. Must not contain any sensitive information such as private keys, transaction IDs, or wallet addresses.
24
23
  */
@@ -47,8 +46,10 @@ export class EthLikeError extends Error {
47
46
 
48
47
  // Define regex patterns for sensitive information
49
48
  const sensitivePatterns = [
50
- /(?:0x)?[\dA-Fa-f]{64}/g, // Pattern for transaction hashes (hex string of 64 characters, optional 0x prefix)
51
- /(?:0x)?[\dA-Fa-f]{40}/g, // Pattern for wallet addresses (hex string of 40 characters, optional 0x prefix)
49
+ /(?:0x)?[\dA-Fa-f]{20,}/g, // Pattern for hex values of 20 or more characters
50
+ /(?:[A-Za-z]{3,20}\s+){11}[A-Za-z]{3,20}/g, // Pattern for 12-word phrases
51
+ /[1-9A-HJ-NP-Za-km-z]{50,}/g, // Pattern for base58 strings of 50 or more characters
52
+ /(?:[\d+/A-Za-z]{4}){3,}(?:[\d+/A-Za-z]{2}==|[\d+/A-Za-z]{3}=|[\d+/A-Za-z]{4})/g, // Pattern for base64 strings of 12 or more characters
52
53
  ]
53
54
 
54
55
  let filteredHint = hint
@@ -24,6 +24,38 @@ const getUnstaked = ({ accountState, asset }) => {
24
24
  )
25
25
  }
26
26
 
27
+ /**
28
+ * Calculates amount received in staking txs from txLog
29
+ * Staking txs are counted based in the amount received in the tx (tx.coinAmount)
30
+ * It can be different from staked / unstaked / claimed requested amount)
31
+ *
32
+ * Unstaked tx.coinAmount - Staked tx.coinAmount + Claimed tx.coinAmount
33
+ *
34
+ * Depending on the asset, is what tx.coinAmount is:
35
+ *
36
+ * Ethereum: for unstake txs, it's unknown how much of it are rewards and unstaked received (full or partial)
37
+ * Polygon: rewards are transferred in stake and unstake txs
38
+ */
39
+ export const getStakingHistoryBalance = ({ asset, txLog }) => {
40
+ let stakingHistoryBalance = asset.currency.ZERO
41
+ for (const tx of txLog) {
42
+ const successfulTx = tx.confirmations && !tx.failed
43
+ if (successfulTx && tx.data?.txAmount) {
44
+ // only staking txs have tx.data.txAmount set
45
+ // tx.data.txAmount is negative for stake tx type
46
+ const txAmount = asset.currency.defaultUnit(tx.data.txAmount)
47
+ stakingHistoryBalance = stakingHistoryBalance.add(txAmount)
48
+ }
49
+ }
50
+
51
+ return stakingHistoryBalance
52
+ }
53
+
54
+ const getSpendable = ({ asset, balance, txLog, unconfirmedReceived }) => {
55
+ const stakingHistoryBalance = getStakingHistoryBalance({ asset, txLog })
56
+ return balance.add(stakingHistoryBalance).sub(unconfirmedReceived)
57
+ }
58
+
27
59
  /**
28
60
  * Api method to return the balance based on either account state balances or tx history.
29
61
  *
@@ -38,21 +70,28 @@ export const getBalancesFactory = ({ monitorType }) => {
38
70
  const unconfirmedReceived = getUnconfirmedReceivedBalance({ asset, txLog })
39
71
  const unconfirmedSent = getUnconfirmedSentBalance({ asset, txLog })
40
72
 
41
- // balance from txLog / rpc is considered to be total
42
- const balanceWithoutUnconfirmedSent =
43
- monitorType === 'no-history' || isRpcBalanceAsset(asset)
44
- ? getBalanceFromAccountState({ asset, accountState }).sub(unconfirmedSent)
45
- : getBalanceFromTxLog({ txLog, asset })
46
-
47
73
  const staked = getStaked({ asset, accountState })
48
74
  const unstaking = getUnstaking({ asset, accountState })
49
75
  const unstaked = getUnstaked({ asset, accountState })
50
- const total = balanceWithoutUnconfirmedSent
51
- const spendable = balanceWithoutUnconfirmedSent
52
- .sub(unconfirmedReceived)
53
- .sub(staked)
54
- .sub(unstaking)
55
- .sub(unstaked)
76
+
77
+ let total
78
+ let spendable
79
+
80
+ // Balance from accountState is considered total b/c is fetched from rpc
81
+ if (isRpcBalanceAsset(asset) || monitorType === 'no-history') {
82
+ total = getBalanceFromAccountState({ asset, accountState }).sub(unconfirmedSent)
83
+ spendable = total.sub(staked).sub(unstaking).sub(unstaked).sub(unconfirmedReceived)
84
+ } else {
85
+ // Balance from txLog does not include staking rewards
86
+ // spendable and total are calculated differently based on staking txs
87
+ spendable = getSpendable({
88
+ asset,
89
+ balance: getBalanceFromTxLog({ txLog, asset }),
90
+ txLog,
91
+ unconfirmedReceived,
92
+ })
93
+ total = spendable.add(unconfirmedReceived).add(staked).add(unstaking).add(unstaked)
94
+ }
56
95
 
57
96
  return {
58
97
  // new
@@ -5,53 +5,79 @@ import {
5
5
  isEthereumUndelegatePending,
6
6
  } from '../staking/ethereum/staking-utils'
7
7
 
8
- import { isPolygonClaimUndelegate, isPolygonDelegate } from '../staking/matic'
8
+ import { isPolygonClaimUndelegate, isPolygonDelegate, isPolygonUndelegate } from '../staking/matic'
9
9
 
10
- import { decodePolygonStakingTxInputAmount } from './utils.js'
10
+ import {
11
+ decodePolygonStakingTxInputAmount,
12
+ decodeEthLikeStakingTxInputAmount,
13
+ calculateRewardsFromStakeTx,
14
+ } from './utils.js'
11
15
 
12
- const getEthereumStakeTxType = ({ tx }) => {
16
+ const getEthereumStakingTxData = ({ tx, currency }) => {
13
17
  if (
14
18
  ['delegate', 'undelegatePending', 'undelegate', 'claimUndelegate'].some(
15
19
  (stakeTx) => tx.data?.[stakeTx]
16
- )
20
+ ) &&
21
+ tx.coinAmount.isZero
17
22
  )
18
23
  return
19
24
 
25
+ const txAmount = tx.coinAmount.toDefaultString()
26
+
20
27
  if (isEthereumDelegate(tx)) {
21
- return { delegate: tx.coinAmount.toDefaultString() }
28
+ return { delegate: txAmount, txAmount }
22
29
  }
23
30
 
24
31
  // undelegate must be taken in consideration, if unstaked ETH is still
25
32
  // in the pool queue, undelgate transfers staked funds back inmediatly to the user
26
- if (isEthereumUndelegatePending(tx))
33
+ if (isEthereumUndelegatePending(tx)) {
34
+ const undelegatePending = currency
35
+ .baseUnit(decodeEthLikeStakingTxInputAmount(tx))
36
+ .toDefaultString()
27
37
  return {
28
- undelegatePending: tx.coinAmount.toDefaultString(),
38
+ undelegatePending,
39
+ txAmount,
29
40
  }
41
+ }
30
42
 
31
43
  if (isEthereumUndelegate(tx)) {
32
- return { undelegate: tx.coinAmount.toDefaultString() }
44
+ const undelegate = currency.baseUnit(decodeEthLikeStakingTxInputAmount(tx)).toDefaultString()
45
+ return { undelegate, txAmount }
33
46
  }
34
47
 
35
48
  // In the case of the ETH being actually staked and earning,
36
49
  // unstake has a withdraw period, after that, unstaked can be claimed.
37
50
  if (isEthereumClaimUndelegate(tx)) {
38
- return { claimUndelegate: tx.coinAmount.toDefaultString() }
51
+ return { claimUndelegate: txAmount, txAmount }
39
52
  }
40
53
  }
41
54
 
42
- const getPolygonStakeTxType = ({ tx, currency }) => {
43
- if (tx.coinAmount.isZero) return
55
+ const getPolygonStakingTxData = ({ tx, currency }) => {
56
+ if (['delegate', 'undelegate', 'claimUndelegate'].some((stakeTx) => tx.data?.[stakeTx])) return
57
+
58
+ const txAmount = tx.coinAmount.toDefaultString()
44
59
 
45
60
  if (isPolygonDelegate(tx)) {
46
- const stakeTxAmount = currency.baseUnit(decodePolygonStakingTxInputAmount(tx)).toDefaultString()
47
- return { delegate: stakeTxAmount }
61
+ const delegate = currency.baseUnit(decodePolygonStakingTxInputAmount(tx)).toDefaultString()
62
+ // MATIC returned in unstake tx is always reward
63
+ const rewards = calculateRewardsFromStakeTx({ tx, currency })
64
+ return { delegate, txAmount, ...(rewards ? { rewards } : {}) }
48
65
  }
49
66
 
50
- if (isPolygonClaimUndelegate(tx)) return { undelegate: tx.coinAmount.toDefaultString() }
67
+ if (isPolygonUndelegate(tx)) {
68
+ const undelegate = currency.baseUnit(decodePolygonStakingTxInputAmount(tx)).toDefaultString()
69
+ // MATIC returned in unstake tx is always reward
70
+ const rewards = txAmount
71
+ return { undelegate, txAmount, rewards }
72
+ }
73
+
74
+ if (isPolygonClaimUndelegate(tx)) {
75
+ return { claimUndelegate: txAmount, txAmount }
76
+ }
51
77
  }
52
78
 
53
79
  export const assetStakingTxData = {
54
- polygon: getPolygonStakeTxType,
55
- ethereum: getEthereumStakeTxType,
56
- ethereumholesky: getEthereumStakeTxType,
80
+ polygon: getPolygonStakingTxData,
81
+ ethereum: getEthereumStakingTxData,
82
+ ethereumholesky: getEthereumStakingTxData,
57
83
  }
@@ -1,7 +1,6 @@
1
1
  import { assetStakingTxData } from './asset-staking-tx-data'
2
- import { getStakeTxAmount } from './get-asset-tx-amount'
3
2
 
4
- const getTxStakeData = ({ assetName, currency, tx }) => {
3
+ const getTxStakingData = ({ assetName, currency, tx }) => {
5
4
  return assetStakingTxData[assetName]({ tx, currency })
6
5
  }
7
6
 
@@ -11,13 +10,12 @@ const processTxLog = async ({ asset, assetClientInterface: aci, walletAccount, b
11
10
 
12
11
  const newTxs = []
13
12
  for (const tx of txs) {
14
- const txStakeData = getTxStakeData({ assetName, currency, tx })
15
- if (txStakeData) {
16
- const txAmount = getStakeTxAmount[assetName]({ tx, currency, type: txStakeData })
13
+ const stakingData = getTxStakingData({ assetName, currency, tx })
14
+ if (stakingData) {
17
15
  newTxs.push({
18
16
  ...tx,
19
- coinAmount: txAmount,
20
- data: { ...tx.data, ...txStakeData },
17
+ coinAmount: currency.ZERO,
18
+ data: { ...tx.data, ...stakingData },
21
19
  })
22
20
  }
23
21
  }
@@ -18,7 +18,7 @@ export const decodePolygonStakingTxInputAmount = (tx) => {
18
18
  return amount
19
19
  }
20
20
 
21
- export const calculateTxAmountAndRewardFromStakeTx = ({ tx, currency }) => {
21
+ export const calculateRewardsFromStakeTx = ({ tx, currency }) => {
22
22
  const stakedAmount = currency.baseUnit(decodePolygonStakingTxInputAmount(tx))
23
23
  const { reward } = tx.data
24
24
  // stake tx might have rewards in it,
@@ -35,12 +35,8 @@ export const calculateTxAmountAndRewardFromStakeTx = ({ tx, currency }) => {
35
35
  if (stakeTxContainsReward) {
36
36
  const txAmount = stakedAmount.sub(tx.coinAmount.abs()).abs()
37
37
  // eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only -- TODO: Fix this the next time the file is edited.
38
- tx.data.reward = txAmount.toBaseString()
39
- return txAmount
38
+ return txAmount.toBaseString()
40
39
  }
41
-
42
- // no rewards, set stake tx amount to ZERO
43
- return currency.ZERO
44
40
  }
45
41
 
46
42
  export const decodeEthLikeStakingTxInputAmount = (tx) => {
@@ -1,26 +0,0 @@
1
- import { calculateTxAmountAndRewardFromStakeTx } from './utils.js'
2
-
3
- const getPolygonTxAmount = ({ currency, tx, type }) => {
4
- if ('delegate' in type) {
5
- return calculateTxAmountAndRewardFromStakeTx({ tx, currency })
6
- }
7
-
8
- return currency.ZERO
9
- }
10
-
11
- const getEthereumTxAmount = ({ currency, tx, type }) => {
12
- const isStakingTx = ['delegate', 'undelegatePending', 'undelegate', 'claimUndelegate'].some(
13
- (stakingType) => type[stakingType]
14
- )
15
- if (isStakingTx) {
16
- return currency.ZERO
17
- }
18
-
19
- return tx.coinAmount
20
- }
21
-
22
- export const getStakeTxAmount = {
23
- polygon: getPolygonTxAmount,
24
- ethereum: getEthereumTxAmount,
25
- ethereumholesky: getEthereumTxAmount,
26
- }