@exodus/ethereum-api 7.3.2 → 7.5.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "7.3.2",
3
+ "version": "7.5.0",
4
4
  "description": "Ethereum Api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -57,5 +57,5 @@
57
57
  "cross-fetch": "^3.1.5",
58
58
  "delay": "4.0.1"
59
59
  },
60
- "gitHead": "06a32175d0e18e2fca6c74e55313b68ce0c2b402"
60
+ "gitHead": "8066c2fd98a9065156d93108e42aa01f40aecf57"
61
61
  }
@@ -21,6 +21,7 @@ import {
21
21
  signUnsignedTxWithSigner,
22
22
  validate,
23
23
  signMessage,
24
+ signMessageWithSigner,
24
25
  signHardwareFactory,
25
26
  DEFAULT_FEE_MONITOR_INTERVAL,
26
27
  } from '@exodus/ethereum-lib'
@@ -34,8 +35,9 @@ import { getEffectiveGasPrice, getFeeFactory } from './get-fee'
34
35
  import { createEthereumHooks } from './hooks'
35
36
  import { createStakingApi } from './staking-api'
36
37
  import { txSendFactory } from './tx-send'
37
- import { signMessageWithSigner } from '@exodus/ethereum-lib/src/sign-message'
38
38
  import { serverBasedFeeMonitorFactoryFactory } from './fee-monitor'
39
+ import { createGetBalanceForAddress } from './get-balance-for-address'
40
+ import { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas'
39
41
 
40
42
  export const createAssetFactory = ({
41
43
  assetsList,
@@ -43,7 +45,6 @@ export const createAssetFactory = ({
43
45
  AccountState: CustomAccountState,
44
46
  monitorInterval,
45
47
  nfts = false,
46
- getFee: customGetFee,
47
48
  useEip1191ChainIdChecksum = false,
48
49
  customTokens = true,
49
50
  isTestnet = false,
@@ -53,6 +54,7 @@ export const createAssetFactory = ({
53
54
  customBip44,
54
55
  customCreateGetKeyIdentifier,
55
56
  feeMonitorInterval,
57
+ l1GasOracleAddress, // l1 extra fee for base and opto
56
58
  }) => {
57
59
  assert(assetsList, 'assetsList is required')
58
60
  assert(feeData, 'feeData is required')
@@ -114,6 +116,7 @@ export const createAssetFactory = ({
114
116
  const createFeeMonitor = serverBasedFeeMonitorFactoryFactory({
115
117
  assetName: base.name,
116
118
  interval: config.feeMonitorInterval || feeMonitorInterval || DEFAULT_FEE_MONITOR_INTERVAL,
119
+ eip1559Enabled: feeData.eip1559Enabled, // this is not updated via remote config. Should it be?
117
120
  })
118
121
 
119
122
  const features = {
@@ -126,6 +129,7 @@ export const createAssetFactory = ({
126
129
  nfts,
127
130
  noHistory: monitorType === 'no-history',
128
131
  signWithSigner: true,
132
+ signMessageWithSigner: true,
129
133
  ...(supportsStaking && { staking: {} }),
130
134
  }
131
135
 
@@ -165,6 +169,17 @@ export const createAssetFactory = ({
165
169
  const defaultAddressPath = 'm/0/0'
166
170
 
167
171
  const sendTx = txSendFactory({ assetClientInterface })
172
+
173
+ const estimateL1DataFee = l1GasOracleAddress
174
+ ? estimateL1DataFeeFactory({ l1GasOracleAddress, server })
175
+ : undefined
176
+
177
+ const originalGetFee = getFeeFactory({ gasLimit })
178
+
179
+ const getFee = l1GasOracleAddress
180
+ ? getL1GetFeeFactory({ asset, originalGetFee })
181
+ : originalGetFee
182
+
168
183
  const api = {
169
184
  addressHasHistory,
170
185
  broadcastTx: (...args) => server.sendRawTransaction(...args),
@@ -176,9 +191,10 @@ export const createAssetFactory = ({
176
191
  defaultAddressPath,
177
192
  features,
178
193
  getBalances,
194
+ getBalanceForAddress: createGetBalanceForAddress({ asset, server }),
179
195
  getConfirmationsNumber: () => confirmationNumber,
180
196
  getDefaultAddressPath: () => defaultAddressPath,
181
- getFee: customGetFee || getFeeFactory({ gasLimit }),
197
+ getFee,
182
198
  getFeeData: () => feeData,
183
199
  getKeyIdentifier: createGetKeyIdentifier({ bip44, allowMetaMaskCompat }),
184
200
  getSupportedPurposes: () => [44],
@@ -207,6 +223,8 @@ export const createAssetFactory = ({
207
223
  keys,
208
224
  address,
209
225
  api,
226
+ estimateL1DataFee,
227
+ server,
210
228
  ...(erc20FuelBuffer && { erc20FuelBuffer }),
211
229
  ...(fuelThreshold && { fuelThreshold: asset.currency.defaultUnit(fuelThreshold) }),
212
230
  getEffectiveGasPrice,
@@ -160,6 +160,21 @@ export default class ApiCoinNodesServer extends EventEmitter {
160
160
  // for fee monitor
161
161
  getGasPrice = this.gasPrice
162
162
 
163
+ async getLatestBlock() {
164
+ const request = this.buildRequest({
165
+ method: 'eth_getBlockByNumber',
166
+ params: ['latest', false],
167
+ })
168
+ return this.sendRequest(request)
169
+ }
170
+
171
+ async getBaseFeePerGas() {
172
+ const response = await this.getLatestBlock()
173
+ if (response.baseFeePerGas) {
174
+ return fromHexToString(response.baseFeePerGas)
175
+ }
176
+ }
177
+
163
178
  async estimateGas(...params) {
164
179
  const request = this.estimateGasRequest(...params)
165
180
  return this.sendRequest(request)
@@ -126,6 +126,17 @@ export function create(defaultURL, ensAssetName) {
126
126
  return requestWithRetry('proxy', { method: 'eth_gasPrice' })
127
127
  },
128
128
 
129
+ async getLatestBlock() {
130
+ return this.getBlockByNumber('latest')
131
+ },
132
+
133
+ async getBaseFeePerGas() {
134
+ const response = await this.getLatestBlock()
135
+ if (response.baseFeePerGas) {
136
+ return fromHexToString(response.baseFeePerGas)
137
+ }
138
+ },
139
+
129
140
  async getTransactionCount(address, tag = 'latest') {
130
141
  return requestWithRetry('proxy', { method: 'eth_getTransactionCount', address, tag })
131
142
  },
@@ -366,6 +366,17 @@ export default class ClarityServer extends EventEmitter {
366
366
  return this.sendRequest(request)
367
367
  }
368
368
 
369
+ async getLatestBlock() {
370
+ return this.getBlockByNumber('latest')
371
+ }
372
+
373
+ async getBaseFeePerGas() {
374
+ const response = await this.getLatestBlock()
375
+ if (response.baseFeePerGas) {
376
+ return fromHexToString(response.baseFeePerGas)
377
+ }
378
+ }
379
+
369
380
  async getBlockByHash(...params) {
370
381
  const request = this.getBlockByHashRequest(...params)
371
382
  return this.sendRequest(request)
@@ -1,6 +1,8 @@
1
- import { EthereumLikeFeeMonitor } from '@exodus/ethereum-lib'
1
+ import { FeeMonitor } from '@exodus/asset-lib'
2
2
  import assert from 'minimalistic-assert'
3
3
  import { getServerByName } from '../exodus-eth-server'
4
+ import { DEFAULT_FEE_MONITOR_INTERVAL } from '@exodus/ethereum-lib'
5
+ import { fromHexToString } from '../number-utils'
4
6
 
5
7
  /**
6
8
  * Generic eth server based fee monitor.
@@ -16,24 +18,37 @@ import { getServerByName } from '../exodus-eth-server'
16
18
  * }
17
19
  */
18
20
 
19
- export const serverBasedFeeMonitorFactoryFactory = ({ assetName, interval }) => {
21
+ export const serverBasedFeeMonitorFactoryFactory = ({
22
+ assetName,
23
+ interval = DEFAULT_FEE_MONITOR_INTERVAL,
24
+ eip1559Enabled,
25
+ }) => {
20
26
  assert(assetName, 'assetName is required')
21
-
22
27
  // NOTE: Using getServerByName for simplicity now but
23
28
  // remove getServerByName and convert server to a param instead.
24
29
  // This is to avoid global references, static creation, remove the chain specific map and allow IOC
25
30
  const server = getServerByName(assetName)
26
- const getGasPrice = (...args) => server.getGasPrice(...args)
27
- const FeeMonitorClass = class ServerBaseEthereumFeeMonitor extends EthereumLikeFeeMonitor {
31
+
32
+ const FeeMonitorClass = class ServerBaseEthereumFeeMonitor extends FeeMonitor {
28
33
  constructor({ updateFee }) {
29
34
  assert(updateFee, 'updateFee is required')
30
35
  super({
31
36
  updateFee,
32
37
  assetName,
33
- getGasPrice,
34
38
  interval,
35
39
  })
36
40
  }
41
+
42
+ async fetchFee() {
43
+ const gasPrice = fromHexToString(await server.getGasPrice())
44
+
45
+ const baseFeePerGas = eip1559Enabled ? `${await server.getBaseFeePerGas()} wei` : undefined
46
+
47
+ return {
48
+ gasPrice: `${gasPrice} wei`,
49
+ baseFeePerGas,
50
+ }
51
+ }
37
52
  }
38
53
  return (...args) => new FeeMonitorClass(...args)
39
54
  }
@@ -0,0 +1,10 @@
1
+ import assert from 'minimalistic-assert'
2
+
3
+ export const createGetBalanceForAddress = ({ asset, server }) => {
4
+ assert(asset, 'asset is required')
5
+ assert(server, 'server is required')
6
+ return async (address) => {
7
+ const balances = await server.getBalance(address)
8
+ return asset.currency.baseUnit(balances.confirmed.value)
9
+ }
10
+ }
@@ -2,21 +2,6 @@ import { isRpcBalanceAsset } from '@exodus/ethereum-lib'
2
2
 
3
3
  import { get } from 'lodash'
4
4
 
5
- const fixBalance = ({ txLog, balance }) => {
6
- for (const tx of txLog) {
7
- // TODO: pending can only be less than a few minutes old, we can only search the latest txs to improve performance
8
- if (tx.sent && tx.pending && !tx.error) {
9
- // coinAmount is negative for sent tx
10
- balance = balance.sub(tx.coinAmount.abs())
11
- if (tx.coinAmount.unitType.equals(tx.feeAmount.unitType)) {
12
- balance = balance.sub(tx.feeAmount)
13
- }
14
- }
15
- }
16
-
17
- return balance
18
- }
19
-
20
5
  const getBalanceFromAccountState = ({ asset, accountState }) => {
21
6
  const isBase = asset.name === asset.baseAsset.name
22
7
  return get(
@@ -44,14 +29,33 @@ const getUnstaked = ({ accountState, asset }) => {
44
29
  )
45
30
  }
46
31
 
47
- function getBasicSpendable({ asset, accountState, txLog }) {
48
- const balance = isRpcBalanceAsset(asset)
49
- ? getBalanceFromAccountState({ asset, accountState })
50
- : getBalanceFromTxLog({ txLog, asset })
32
+ const getUnconfirmedSentBalanceFromTxLog = ({ asset, txLog }) => {
33
+ let result = asset.currency.ZERO
34
+
35
+ for (const tx of txLog) {
36
+ const isUnconfirmed = !tx.failed && tx.pending
37
+
38
+ if (isUnconfirmed && tx.sent) {
39
+ result = result.add(tx.coinAmount.abs())
40
+ if (tx.feeAmount && tx.coinAmount.unitType.equals(tx.feeAmount.unitType)) {
41
+ result = result.add(tx.feeAmount.abs())
42
+ }
43
+ }
44
+ }
51
45
 
52
- const shouldFixBalance = isRpcBalanceAsset(asset)
46
+ return result
47
+ }
53
48
 
54
- return shouldFixBalance ? fixBalance({ txLog, balance }) : balance
49
+ const getUnconfirmedReceivedBalanceFromTxLog = ({ asset, txLog }) => {
50
+ let result = asset.currency.ZERO
51
+ for (const tx of txLog) {
52
+ const isUnconfirmed = !tx.failed && tx.pending
53
+ if (isUnconfirmed && !tx.sent) {
54
+ result = result.add(tx.coinAmount.abs())
55
+ }
56
+ }
57
+
58
+ return result
55
59
  }
56
60
 
57
61
  /**
@@ -63,13 +67,18 @@ function getBasicSpendable({ asset, accountState, txLog }) {
63
67
  * @returns {{balance}|null} an object with the balance or null if the balance is unknown
64
68
  */
65
69
  export const getBalances = ({ asset, txLog, accountState }) => {
66
- const spendable = getBasicSpendable({ asset, accountState, txLog })
70
+ const unconfirmedReceived = getUnconfirmedReceivedBalanceFromTxLog({ asset, txLog })
71
+ const unconfirmedSent = getUnconfirmedSentBalanceFromTxLog({ asset, txLog })
67
72
 
73
+ const balanceWithoutUnconfirmedSent = isRpcBalanceAsset(asset)
74
+ ? getBalanceFromAccountState({ asset, accountState }).sub(unconfirmedSent)
75
+ : getBalanceFromTxLog({ txLog, asset })
76
+
77
+ const spendable = balanceWithoutUnconfirmedSent.sub(unconfirmedReceived)
68
78
  const staked = getStaked({ asset, accountState })
69
79
  const unstaking = getUnstaking({ asset, accountState })
70
80
  const unstaked = getUnstaked({ asset, accountState })
71
-
72
- const total = spendable.add(staked).add(unstaking).add(unstaked)
81
+ const total = balanceWithoutUnconfirmedSent.add(staked).add(unstaking).add(unstaked)
73
82
 
74
83
  return {
75
84
  // new
@@ -78,6 +87,8 @@ export const getBalances = ({ asset, txLog, accountState }) => {
78
87
  staked,
79
88
  unstaking,
80
89
  unstaked,
90
+ unconfirmedReceived,
91
+ unconfirmedSent,
81
92
  // legacy
82
93
  balance: total,
83
94
  spendableBalance: spendable,
@@ -3,19 +3,48 @@ import { createEthereumJsTx, createContract } from '@exodus/ethereum-lib'
3
3
  import { getServerByName } from '../exodus-eth-server'
4
4
  import { GAS_ORACLE_ADDRESS } from './addresses'
5
5
  import { fromHexToBigInt } from '../number-utils'
6
+ import assert from 'minimalistic-assert'
6
7
 
7
- const gasContract = createContract(GAS_ORACLE_ADDRESS, 'optimismGasOracle')
8
+ export const estimateL1DataFeeFactory = ({ l1GasOracleAddress, server }) => {
9
+ assert(l1GasOracleAddress, 'l1GasOracleAddress is required')
10
+ assert(server, 'server is required')
11
+ const gasContract = createContract(l1GasOracleAddress, 'optimismGasOracle')
12
+ return async ({ unsignedTx }) => {
13
+ const ethjsTx = createEthereumJsTx(unsignedTx)
14
+ const serialized = ethjsTx.serialize()
15
+ const callData = gasContract.getL1Fee.build(serialized)
16
+ const buffer = Buffer.from(callData)
17
+ const data = ethUtil.bufferToHex(buffer)
18
+ const hex = await server.ethCall({ to: l1GasOracleAddress, data }, 'latest')
19
+ const l1DataFee = fromHexToBigInt(hex)
20
+ const padFee = l1DataFee / BigInt(4)
21
+ const maxL1DataFee = l1DataFee + padFee
22
+ return maxL1DataFee.toString()
23
+ }
24
+ }
25
+
26
+ export const getL1GetFeeFactory = ({ asset, originalGetFee }) => {
27
+ return ({ feeOpts, ...args }) => {
28
+ const { fee, ...rest } = originalGetFee(args)
29
+ if (!feeOpts?.optimismL1DataFee) {
30
+ // hardcoded optimism name is for back compatiblity, it could be base!
31
+ // better to rename
32
+ return { fee, ...rest }
33
+ }
8
34
 
9
- export async function estimateOptimismL1DataFee({ unsignedTx }) {
10
- const ethjsTx = createEthereumJsTx(unsignedTx)
11
- const serialized = ethjsTx.serialize()
12
- const callData = gasContract.getL1Fee.build(serialized)
13
- const buffer = Buffer.from(callData)
14
- const data = ethUtil.bufferToHex(buffer)
15
- const server = getServerByName('optimism')
16
- const hex = await server.ethCall({ to: GAS_ORACLE_ADDRESS, data }, 'latest')
17
- const l1DataFee = fromHexToBigInt(hex)
18
- const padFee = l1DataFee / BigInt(4)
19
- const maxL1DataFee = l1DataFee + padFee
20
- return maxL1DataFee.toString()
35
+ const l1DataFee = asset.currency.baseUnit(feeOpts.optimismL1DataFee)
36
+ return { fee: fee.add(l1DataFee), ...rest }
37
+ }
21
38
  }
39
+
40
+ /**
41
+ * Back-compatibility
42
+ *
43
+ * @deprecated use eth-asset.estimateL1DataFee
44
+ * @param unsignedTx
45
+ * @returns {Promise<string>}
46
+ */
47
+ export const estimateOptimismL1DataFee = estimateL1DataFeeFactory({
48
+ server: getServerByName('optimism'),
49
+ l1GasOracleAddress: GAS_ORACLE_ADDRESS,
50
+ })
@@ -34,6 +34,12 @@ const txSendFactory = ({ assetClientInterface }) => {
34
34
  const assets = await assetClientInterface.getAssetsForNetwork({ baseAssetName: baseAsset.name })
35
35
  let eip1559Enabled = baseAsset.name === 'ethereum' // TODO: temp override, clean up use of eip1559Enabled flag and default to always true
36
36
 
37
+ // Using a default zero value to not break code relying on the `tx.feeAmount` property.
38
+ // For example, some exchange providers don't supply this.
39
+ if (!feeAmount) {
40
+ feeAmount = asset.baseAsset.currency.ZERO
41
+ }
42
+
37
43
  const fromAddress = await assetClientInterface.getReceiveAddress({
38
44
  assetName: baseAsset.name,
39
45
  walletAccount,