@exodus/ethereum-api 7.2.1 → 7.3.1

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/src/index.js CHANGED
@@ -11,3 +11,6 @@ export * from './staking'
11
11
  export * from './simulate-tx'
12
12
  export * from './allowance'
13
13
  export * from './optimism-gas'
14
+ export * from './number-utils'
15
+ export { txSendFactory, getFeeInfo } from './tx-send'
16
+ export { createAssetFactory } from './create-asset'
@@ -0,0 +1,8 @@
1
+ export function fromHexToString(value) {
2
+ return fromHexToBigInt(value).toString()
3
+ }
4
+
5
+ export function fromHexToBigInt(value) {
6
+ const hex = value.startsWith('0x') ? value : '0x' + value
7
+ return BigInt(hex)
8
+ }
@@ -1,8 +1,8 @@
1
- import BN from 'bn.js'
2
1
  import * as ethUtil from '@exodus/ethereumjs-util'
3
2
  import { createEthereumJsTx, createContract } from '@exodus/ethereum-lib'
4
3
  import { getServerByName } from '../exodus-eth-server'
5
4
  import { GAS_ORACLE_ADDRESS } from './addresses'
5
+ import { fromHexToBigInt } from '../number-utils'
6
6
 
7
7
  const gasContract = createContract(GAS_ORACLE_ADDRESS, 'optimismGasOracle')
8
8
 
@@ -14,8 +14,8 @@ export async function estimateOptimismL1DataFee({ unsignedTx }) {
14
14
  const data = ethUtil.bufferToHex(buffer)
15
15
  const server = getServerByName('optimism')
16
16
  const hex = await server.ethCall({ to: GAS_ORACLE_ADDRESS, data }, 'latest')
17
- const l1DataFee = new BN(hex.slice(2), 16)
18
- const padFee = l1DataFee.div(new BN(4, 10))
19
- const maxL1DataFee = l1DataFee.add(padFee)
17
+ const l1DataFee = fromHexToBigInt(hex)
18
+ const padFee = l1DataFee / BigInt(4)
19
+ const maxL1DataFee = l1DataFee + padFee
20
20
  return maxL1DataFee.toString()
21
21
  }
@@ -1,9 +1,9 @@
1
1
  import { estimateGasLimit } from '../../gas-estimation'
2
2
  import { getServer } from '../../exodus-eth-server'
3
3
  import { isNumberUnit } from '@exodus/currency'
4
- import BN from 'bn.js'
5
4
  import { stakingProviderClientFactory } from '../staking-provider-client'
6
5
  import { EthereumStaking } from './api'
6
+ import { fromHexToBigInt } from '../../number-utils'
7
7
 
8
8
  const extraGasLimit = 20_000 // extra gas Limit to prevent tx failing if something change on pool state (till tx is in mempool)
9
9
 
@@ -233,12 +233,12 @@ export function createEthereumStakingService({
233
233
  gasPrice = await getServer(asset).gasPrice()
234
234
  }
235
235
 
236
- gasPrice = parseInt(gasPrice, 16)
237
- const fee = new BN(gasPrice).mul(new BN(gasLimit + extraGasLimit))
236
+ gasPrice = fromHexToBigInt(gasPrice)
237
+ const fee = gasPrice * BigInt(gasLimit + extraGasLimit)
238
238
 
239
239
  return {
240
240
  gasLimit,
241
- gasPrice: asset.currency.baseUnit(gasPrice),
241
+ gasPrice: asset.currency.baseUnit(gasPrice.toString()),
242
242
  fee: asset.currency.baseUnit(fee.toString()),
243
243
  }
244
244
  }
@@ -0,0 +1,5 @@
1
+ export function createStakingApi({ network }) {
2
+ return {
3
+ isStaking: ({ accountState }) => accountState.staking[network]?.isDelegating,
4
+ }
5
+ }
@@ -1,4 +1,3 @@
1
- import BN from 'bn.js'
2
1
  import { BaseMonitor } from '@exodus/asset-lib'
3
2
  import { getAssetAddresses, isRpcBalanceAsset } from '@exodus/ethereum-lib'
4
3
  import { isEmpty } from 'lodash'
@@ -10,6 +9,7 @@ import {
10
9
  excludeUnchangedTokenBalances,
11
10
  } from './monitor-utils'
12
11
  import { getLogItemsFromServerTx, getDeriveDataNeededForTick, filterEffects } from './clarity-utils'
12
+ import { fromHexToString } from '../number-utils'
13
13
 
14
14
  export class ClarityMonitor extends BaseMonitor {
15
15
  constructor({ server, config, ...args }) {
@@ -250,8 +250,7 @@ export class ClarityMonitor extends BaseMonitor {
250
250
  const entries = pairs.map((pair, idx) => {
251
251
  const balanceHex = responses[idx]
252
252
  const name = pair[0]
253
- const hex = balanceHex.startsWith('0x') ? balanceHex.slice(2) : balanceHex
254
- const balance = new BN(hex, 'hex').toString()
253
+ const balance = fromHexToString(balanceHex)
255
254
  return [name, balance]
256
255
  })
257
256
  return Object.fromEntries(entries)
@@ -273,13 +272,9 @@ export class ClarityMonitor extends BaseMonitor {
273
272
 
274
273
  async updateGasPrice({ gasPrice, baseFeePerGas }) {
275
274
  try {
276
- const gasPriceHex = gasPrice?.startsWith('0x') ? gasPrice.slice(2) : gasPrice
277
- const feeConfig = { gasPrice: `${new BN(gasPriceHex, 'hex').toString()} wei` }
275
+ const feeConfig = { gasPrice: `${fromHexToString(gasPrice).toString()} wei` }
278
276
  if (baseFeePerGas) {
279
- const baseFeePerGasHex = baseFeePerGas?.startsWith('0x')
280
- ? baseFeePerGas.slice(2)
281
- : baseFeePerGas
282
- feeConfig.baseFeePerGas = `${new BN(baseFeePerGasHex, 'hex').toString()} wei`
277
+ feeConfig.baseFeePerGas = `${fromHexToString(baseFeePerGas)} wei`
283
278
  }
284
279
 
285
280
  this.logger.debug(
@@ -1,4 +1,3 @@
1
- import BN from 'bn.js'
2
1
  import { getServer } from '../exodus-eth-server'
3
2
  import { isRpcBalanceAsset, getAssetAddresses } from '@exodus/ethereum-lib'
4
3
 
@@ -21,6 +20,7 @@ import {
21
20
  import { isEmpty } from 'lodash'
22
21
 
23
22
  import { BaseMonitor } from '@exodus/asset-lib'
23
+ import { fromHexToString } from '../number-utils'
24
24
 
25
25
  // The base ethereum monitor class handles listening for, fetching,
26
26
  // formatting, and populating-to-state all ETH/ETC/ERC20 transactions.
@@ -211,8 +211,7 @@ export class EthereumMonitor extends BaseMonitor {
211
211
  const server = this.server
212
212
  if (isRpcBalanceAsset(asset)) {
213
213
  const result = await server.getBalanceProxied(ourWalletAddress)
214
- const hex = result.startsWith('0x') ? result.slice(2) : result
215
- const balance = new BN(hex, 'hex').toString()
214
+ const balance = fromHexToString(result)
216
215
  newAccountState.balance = asset.currency.baseUnit(balance)
217
216
  }
218
217
 
@@ -1,4 +1,3 @@
1
- import BN from 'bn.js'
2
1
  import { getServer } from '../exodus-eth-server'
3
2
  import { DEFAULT_SERVER_URLS } from '@exodus/ethereum-lib'
4
3
  import { Tx } from '@exodus/models'
@@ -14,6 +13,7 @@ import { isEmpty, unionBy, zipObject } from 'lodash'
14
13
  import { BaseMonitor } from '@exodus/asset-lib'
15
14
 
16
15
  import { UNCONFIRMED_TX_LIMIT } from './monitor-utils/get-derive-transactions-to-check'
16
+ import { fromHexToString } from '../number-utils'
17
17
 
18
18
  // The base ethereum monitor no history class handles listening for assets with no history
19
19
 
@@ -57,8 +57,7 @@ export class EthereumNoHistoryMonitor extends BaseMonitor {
57
57
  const entries = pairs.map((pair, idx) => {
58
58
  const balanceHex = responses[idx]
59
59
  const name = pair[0]
60
- const hex = balanceHex.startsWith('0x') ? balanceHex.slice(2) : balanceHex
61
- const balance = new BN(hex, 'hex').toString()
60
+ const balance = fromHexToString(balanceHex)
62
61
  return [name, balance]
63
62
  })
64
63
  return Object.fromEntries(entries)
@@ -0,0 +1,48 @@
1
+ import {
2
+ isEthereumClaimUndelegate,
3
+ isEthereumDelegate,
4
+ isEthereumUndelegate,
5
+ isEthereumUndelegatePending,
6
+ } from '../staking/ethereum/staking-utils'
7
+
8
+ import { isPolygonClaimUndelegate, isPolygonDelegate } from '../staking/matic'
9
+
10
+ import {
11
+ decodePolygonStakingTxInputAmount,
12
+ isEthereumTxCountedForRewards,
13
+ isEthereumUndelegateTx,
14
+ } from './utils.js'
15
+
16
+ const getEthereumStakeTxType = ({ tx }) => {
17
+ // only unstake txs with ZERO amount are used to calculate rewards
18
+ const skipTx = tx.coinAmount.isZero && !isEthereumUndelegateTx(tx)
19
+ if (skipTx) return
20
+
21
+ if (isEthereumDelegate(tx)) return { delegate: tx.coinAmount.toDefaultString() }
22
+ // undelegate must be taken in consideration, if unstaked ETH is still
23
+ // in the pool queue, undelgate transfers staked funds back inmediatly to the user
24
+ if (isEthereumUndelegatePending(tx)) return { undelegatePending: tx.coinAmount.toDefaultString() }
25
+ if (isEthereumUndelegate(tx) && !isEthereumTxCountedForRewards(tx))
26
+ return { undelegate: tx.coinAmount.toDefaultString() }
27
+ // In the case of the ETH being actually staked and earning,
28
+ // unstake has a withdraw period, after that, unstaked can be claimed.
29
+ if (isEthereumClaimUndelegate(tx) && !isEthereumTxCountedForRewards(tx))
30
+ return { claimUndelegate: tx.coinAmount.toDefaultString() }
31
+ }
32
+
33
+ const getPolygonStakeTxType = ({ tx, currency }) => {
34
+ if (tx.coinAmount.isZero) return
35
+
36
+ if (isPolygonDelegate(tx)) {
37
+ const stakeTxAmount = currency.baseUnit(decodePolygonStakingTxInputAmount(tx)).toDefaultString()
38
+ return { delegate: stakeTxAmount }
39
+ }
40
+
41
+ if (isPolygonClaimUndelegate(tx)) return { undelegate: tx.coinAmount.toDefaultString() }
42
+ }
43
+
44
+ export const assetStakingTxData = {
45
+ polygon: getPolygonStakeTxType,
46
+ ethereum: getEthereumStakeTxType,
47
+ ethereumholesky: getEthereumStakeTxType,
48
+ }
@@ -0,0 +1,59 @@
1
+ import { decodeEthLikeStakingTxInputAmount } from './utils'
2
+
3
+ const deriveEthereumTxs = ({ currency }) => {
4
+ let totalUnstaked = currency.ZERO
5
+
6
+ return function ({ tx, txStakeData }) {
7
+ const { claimUndelegate, undelegate } = txStakeData
8
+
9
+ if (claimUndelegate && totalUnstaked.isPositive) {
10
+ // calculate and add a new reward tx based 'totalUnstaked' (unstaked amount counted so far)
11
+ const claimedWithRewards = currency.defaultUnit(claimUndelegate)
12
+ const reward = claimedWithRewards.sub(totalUnstaked)
13
+ const claimed = claimedWithRewards.sub(reward)
14
+ // count again for new rewards in upcoming unstake txs
15
+ totalUnstaked = currency.ZERO
16
+ return reward.isPositive
17
+ ? [
18
+ {
19
+ ...tx,
20
+ coinAmount: reward,
21
+ data: {
22
+ reward: reward.toDefaultString(),
23
+ claim: claimed.toDefaultString(),
24
+ countedForRewards: true,
25
+ },
26
+ },
27
+ ]
28
+ : []
29
+ }
30
+
31
+ if (undelegate) {
32
+ // Only care about amount requested in 'unstake()'; considered to be in progress.
33
+ // Counting unstake (not unstake pending) amount until 'claimUnstake()' is called,
34
+ // then use the counted unstaked amount to derive rewards with:
35
+ // rewards = claimUstaked - totalUnstaked
36
+ const unstakeInProgress = currency.baseUnit(decodeEthLikeStakingTxInputAmount(tx))
37
+ totalUnstaked = totalUnstaked.add(unstakeInProgress)
38
+ return [
39
+ {
40
+ ...tx,
41
+ coinAmount: currency.ZERO,
42
+ data: { ...tx.data, countedForRewards: true },
43
+ },
44
+ ]
45
+ }
46
+
47
+ // undelegate pending excluded as unstaked funds are moved inmediatly to user address
48
+ return []
49
+ }
50
+ }
51
+
52
+ export const deriveAssetTxsFactory = ({ assetName, currency }) => {
53
+ const deriveTxHandlers = {
54
+ ethereum: deriveEthereumTxs({ currency }),
55
+ ethereumholesky: deriveEthereumTxs({ currency }),
56
+ }
57
+
58
+ return deriveTxHandlers[assetName]
59
+ }
@@ -0,0 +1,26 @@
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
+ }
@@ -0,0 +1,36 @@
1
+ import { assetStakingTxData } from './asset-staking-tx-data'
2
+ import { deriveAssetTxsFactory } from './derive-txs'
3
+ import { getStakeTxAmount } from './get-asset-tx-amount'
4
+
5
+ const getTxStakeData = ({ assetName, currency, tx }) => {
6
+ return assetStakingTxData[assetName]({ tx, currency })
7
+ }
8
+
9
+ const processTxLog = async ({ asset, assetClientInterface: aci, walletAccount, batch }) => {
10
+ const { name: assetName, currency } = asset
11
+ const txs = await aci.getTxLog({ assetName, walletAccount })
12
+ const deriveAssetTxs = deriveAssetTxsFactory({ assetName, currency })
13
+
14
+ const newTxs = []
15
+ for (const tx of txs) {
16
+ const txStakeData = getTxStakeData({ assetName, currency, tx })
17
+ if (txStakeData) {
18
+ const txAmount = getStakeTxAmount[assetName]({ tx, currency, type: txStakeData })
19
+ newTxs.push({
20
+ ...tx,
21
+ coinAmount: txAmount,
22
+ data: { ...tx.data, ...txStakeData },
23
+ })
24
+ }
25
+
26
+ if (txStakeData && deriveAssetTxs) {
27
+ newTxs.push(...deriveAssetTxs({ tx, txStakeData }))
28
+ }
29
+ }
30
+
31
+ if (newTxs.length > 0) {
32
+ aci.updateTxLogAndNotifyBatch({ assetName, walletAccount, txs: newTxs, batch })
33
+ }
34
+ }
35
+
36
+ export default processTxLog
@@ -0,0 +1,70 @@
1
+ import { EthereumStaking, isEthereumStakingTx, MaticStakingApi } from '../staking'
2
+ import { asset as ethereum } from '@exodus/ethereum-meta'
3
+ import { asset as ethereumholesky } from '@exodus/ethereumholesky-meta'
4
+
5
+ const polygonStakingApi = new MaticStakingApi()
6
+ const ethereumStakingApi = new EthereumStaking(ethereum)
7
+ const ethereumHoleskyStakingApi = new EthereumStaking(ethereumholesky)
8
+
9
+ export const decodePolygonStakingTxInputAmount = (tx) => {
10
+ const {
11
+ data: { data: txInput },
12
+ } = tx
13
+
14
+ const {
15
+ values: [amount], // stake or unstake amount
16
+ } = polygonStakingApi.validatorShareContract.decodeInput(txInput)
17
+
18
+ return amount
19
+ }
20
+
21
+ export const calculateTxAmountAndRewardFromStakeTx = ({ tx, currency }) => {
22
+ const stakedAmount = currency.baseUnit(decodePolygonStakingTxInputAmount(tx))
23
+ const { reward } = tx.data
24
+ // stake tx might have rewards in it,
25
+ // i.e: https://etherscan.io/tx/0x0a81d266109034a3a70c6f1b9601c105d8caebbd0de652a0619344f9559ae4fa
26
+ // thus reward = txInputAmount(amount to stake) - tx.coinAmount
27
+ // tx.coinAmount is already computed from monitor; incoming - outgoing ERC20 token txs
28
+ const stakeTxContainsReward = !stakedAmount.equals(tx.coinAmount.abs())
29
+
30
+ if (reward) {
31
+ // cache rewards
32
+ return currency.baseUnit(reward)
33
+ }
34
+
35
+ if (stakeTxContainsReward) {
36
+ const txAmount = stakedAmount.sub(tx.coinAmount.abs()).abs()
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
40
+ }
41
+
42
+ // no rewards, set stake tx amount to ZERO
43
+ return currency.ZERO
44
+ }
45
+
46
+ export const decodeEthLikeStakingTxInputAmount = (tx) => {
47
+ const {
48
+ data: { data: txInput },
49
+ coinName: assetName,
50
+ } = tx
51
+
52
+ const ethereumLikeContracts = {
53
+ ethereum: ethereumStakingApi,
54
+ ethereumholesky: ethereumHoleskyStakingApi,
55
+ }
56
+
57
+ const ethLikeContract = ethereumLikeContracts[assetName]
58
+
59
+ const {
60
+ values: [amount], // stake or unstake amount
61
+ } = ethLikeContract.contractPool.decodeInput(txInput)
62
+
63
+ return amount
64
+ }
65
+
66
+ // TODO: fix this in eth libraries
67
+ export const isEthereumUndelegateTx = (tx) =>
68
+ isEthereumStakingTx(tx) && tx.data?.methodId === EthereumStaking.METHODS_IDS.UNSTAKE
69
+
70
+ export const isEthereumTxCountedForRewards = (tx) => !!tx.data?.countedForRewards
@@ -0,0 +1,41 @@
1
+ import { fetchGasLimit } from '../gas-estimation'
2
+
3
+ const getFeeInfo = async function getFeeInfo({
4
+ assetClientInterface,
5
+ asset,
6
+ fromAddress,
7
+ toAddress,
8
+ amount,
9
+ isExchange,
10
+ txInput,
11
+ feeOpts = {},
12
+ }) {
13
+ const {
14
+ gasPrice: gasPrice_,
15
+ tipGasPrice: tipGasPrice_,
16
+ gasPriceEconomicalRate,
17
+ } = await assetClientInterface.getFeeData({
18
+ assetName: asset.name,
19
+ })
20
+
21
+ let { gasLimit, gasPrice = gasPrice_, tipGasPrice = tipGasPrice_ } = feeOpts
22
+
23
+ if (!gasLimit) {
24
+ gasLimit = await fetchGasLimit({
25
+ asset,
26
+ fromAddress,
27
+ toAddress,
28
+ amount,
29
+ txInput,
30
+ throwOnError: false,
31
+ isContract: feeOpts.isContract ?? undefined,
32
+ })
33
+ }
34
+
35
+ const economicalFeeMultiplier = isExchange ? 1 : gasPriceEconomicalRate || 1
36
+
37
+ const fee = gasPrice.mul(economicalFeeMultiplier).mul(gasLimit)
38
+ return { gasPrice, gasLimit, fee, tipGasPrice }
39
+ }
40
+
41
+ export default getFeeInfo
@@ -0,0 +1,2 @@
1
+ export { default as txSendFactory } from './tx-send'
2
+ export { default as getFeeInfo } from './get-fee-info'