@exodus/ethereum-api 8.21.2 → 8.22.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/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
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.22.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.21.3...@exodus/ethereum-api@8.22.0) (2024-12-02)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: gasPriceMaximumRate in evm fee data (#4578)
13
+
14
+
15
+
16
+ ## [8.21.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.21.2...@exodus/ethereum-api@8.21.3) (2024-11-27)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+
22
+ * fix: handling JSON RPC responses in Clarity V2 (#4587)
23
+
24
+ * fix: improve nonce calculation for EVMs and fix drop txs in no history (#4583)
25
+
26
+
27
+
6
28
  ## [8.21.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.21.1...@exodus/ethereum-api@8.21.2) (2024-11-15)
7
29
 
8
30
  **Note:** Version bump only for package @exodus/ethereum-api
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @exodus/ethereum-api
2
+
3
+ Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Ethereum and EVM-based blockchains. See [Asset Packages](../../docs/asset-packages.md) for more detail on this package's role.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.21.2",
4
- "description": "Ethereum Api",
3
+ "version": "8.22.0",
4
+ "description": "Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Ethereum and EVM-based blockchains",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "files": [
@@ -64,5 +64,5 @@
64
64
  "type": "git",
65
65
  "url": "git+https://github.com/ExodusMovement/assets.git"
66
66
  },
67
- "gitHead": "95e353cbf4ee280916760a1196b72e74889e475d"
67
+ "gitHead": "9888fd7b7356fd52f0bf92e505071565c8fa19ce"
68
68
  }
@@ -1,6 +1,4 @@
1
1
  import { createMetaDef } from '@exodus/asset'
2
- import { FeeData } from '@exodus/asset-lib'
3
- import { UnitType } from '@exodus/currency'
4
2
  import assert from 'minimalistic-assert'
5
3
 
6
4
  import { createAssetFactory } from './create-asset.js'
@@ -27,8 +25,6 @@ export const createAssetPluginFactory = (config) => {
27
25
  [ticker]: decimals || 18,
28
26
  }
29
27
 
30
- const currency = UnitType.create(units)
31
-
32
28
  // adds lots of validation in config
33
29
  const tokenOverrides = (token) => ({
34
30
  ...token,
@@ -43,7 +39,7 @@ export const createAssetPluginFactory = (config) => {
43
39
  }
44
40
 
45
41
  const assetType = 'ETHEREUM_LIKE'
46
- const { asset, assetsList } = createMetaDef({
42
+ const { assetsList } = createMetaDef({
47
43
  assetParams: {
48
44
  ...meta,
49
45
  name,
@@ -56,61 +52,17 @@ export const createAssetPluginFactory = (config) => {
56
52
  tokensParams: meta.tokens || [],
57
53
  tokenOverrides,
58
54
  })
59
-
60
- const resolveFeeDataConfigDefaults = () => {
61
- const shared = {
62
- tipGasPrice: '0 Gwei',
63
- fuelThreshold: '0 Gwei',
64
- gasPriceEconomicalRate: 0.8,
65
- gasPriceMinimumRate: 0.6,
66
- }
67
-
68
- if (config.eip1559Enabled === true) {
69
- assert(config.baseFeePerGas, 'baseFeePerGas is required')
70
- const baseFeePerGas = currency.parse(config.baseFeePerGas)
71
- const gasPrice = config.gasPrice ? currency.parse(config.gasPrice) : baseFeePerGas.mul(1.5)
72
- return {
73
- gasPrice: gasPrice.toBaseString({ unit: true }),
74
- baseFeePerGas: baseFeePerGas.toBaseString({ unit: true }),
75
- max: gasPrice.mul(5).toBaseString({ unit: true }),
76
- min: gasPrice.div(5).toBaseString({ unit: true }),
77
- eip1559Enabled: config.eip1559Enabled,
78
- ...shared,
79
- ...config.feeData,
80
- }
81
- }
82
-
83
- if (config.eip1559Enabled === false) {
84
- assert(config.gasPrice, 'gasPrice is required')
85
- const gasPrice = currency.parse(config.gasPrice)
86
- return {
87
- gasPrice: gasPrice.toBaseString({ unit: true }),
88
- max: gasPrice.mul(5).toBaseString({ unit: true }),
89
- min: gasPrice.div(5).toBaseString({ unit: true }),
90
- eip1559Enabled: config.eip1559Enabled,
91
- ...shared,
92
- ...config.feeData,
93
- }
94
- }
95
-
96
- return {
97
- gasPrice: '0.02 Gwei',
98
- baseFeePerGas: '0.02 Gwei',
99
- eip1559Enabled: true,
100
- ...shared,
55
+ const feeDataConfig = Object.fromEntries(
56
+ Object.entries({
57
+ gasPrice: config.gasPrice,
58
+ baseFeePerGas: config.baseFeePerGas,
59
+ eip1559Enabled: config.eip1559Enabled,
101
60
  ...config.feeData,
102
- }
103
- }
104
-
105
- const feeData = new FeeData({
106
- config: resolveFeeDataConfigDefaults(),
107
- mainKey: 'gasPrice',
108
- currency: asset.currency,
109
- })
110
-
61
+ }).filter(([_, value]) => value !== undefined)
62
+ )
111
63
  const createAsset = createAssetFactory({
112
64
  assetsList,
113
- feeData,
65
+ feeDataConfig,
114
66
  ...config.plugin,
115
67
  })
116
68
 
@@ -23,6 +23,7 @@ import ms from 'ms'
23
23
  import { addressHasHistoryFactory } from './address-has-history.js'
24
24
  import { createTokenFactory } from './create-token-factory.js'
25
25
  import { createEvmServer } from './exodus-eth-server/index.js'
26
+ import { createFeeData } from './fee-data-factory.js'
26
27
  import { createGetBalanceForAddress } from './get-balance-for-address.js'
27
28
  import { getBalancesFactory } from './get-balances.js'
28
29
  import { getEffectiveGasPrice, getFeeFactory } from './get-fee.js'
@@ -47,7 +48,8 @@ export const createAssetFactory = ({
47
48
  customCreateGetKeyIdentifier,
48
49
  customTokens: defaultCustomTokens = true,
49
50
  erc20FuelBuffer,
50
- feeData,
51
+ feeData: providedFeeData,
52
+ feeDataConfig,
51
53
  feeMonitorInterval,
52
54
  fuelThreshold,
53
55
  isMaxFeeAsset = false,
@@ -62,11 +64,13 @@ export const createAssetFactory = ({
62
64
  forceGasLimitEstimation = false,
63
65
  }) => {
64
66
  assert(assetsList, 'assetsList is required')
65
- assert(feeData, 'feeData is required')
67
+ assert(providedFeeData || feeDataConfig, 'feeData or feeDataConfig is required')
66
68
  assert(serverUrl, 'serverUrl is required')
67
69
  assert(confirmationsNumber, 'confirmationsNumber is required')
68
70
 
69
71
  const base = assetsList.find((asset) => asset.name === asset.baseAssetName)
72
+
73
+ const feeData = providedFeeData || createFeeData({ currency: base.currency, feeDataConfig })
70
74
  assert(base, 'base is required')
71
75
 
72
76
  const chainId = base.chainId
@@ -108,7 +108,9 @@ export default class ClarityServerV2 extends ClarityServer {
108
108
  fetchOptions.body = JSON.stringify(body)
109
109
  }
110
110
 
111
- return fetchJson(url, fetchOptions)
111
+ const response = await fetchJson(url, fetchOptions)
112
+
113
+ return this.handleJsonRPCResponse(response)
112
114
  }
113
115
 
114
116
  async sendRawTransaction(...params) {
@@ -86,6 +86,18 @@ export default class ClarityServer extends EventEmitter {
86
86
  }
87
87
  }
88
88
 
89
+ // See https://www.jsonrpc.org/specification#response_object for details.
90
+ handleJsonRPCResponse(response) {
91
+ const result = response?.result
92
+ const error = response?.error
93
+ if (error || result === undefined) {
94
+ const message = error?.message || error?.code || 'no result'
95
+ throw new Error(`Bad rpc response: ${message}`)
96
+ }
97
+
98
+ return result
99
+ }
100
+
89
101
  async getAllTransactions(params) {
90
102
  const transactions = { pending: [], confirmed: [] }
91
103
  const cursor = await this.getTransactions({
@@ -176,14 +188,8 @@ export default class ClarityServer extends EventEmitter {
176
188
 
177
189
  async sendRequest(request) {
178
190
  const response = await this.sendRpcRequest(request)
179
- const result = response?.result
180
- const error = response?.error
181
- if (error || result === undefined) {
182
- const message = error?.message || error?.code || 'no result'
183
- throw new Error(`Bad rpc response: ${message}`)
184
- }
185
191
 
186
- return result
192
+ return this.handleJsonRPCResponse(response)
187
193
  }
188
194
 
189
195
  async isContract(address) {
@@ -0,0 +1,56 @@
1
+ import { FeeData } from '@exodus/asset-lib'
2
+ import assert from 'minimalistic-assert'
3
+
4
+ const createFeeDataConfigDefaults = ({ currency, feeDataConfig }) => {
5
+ const shared = {
6
+ tipGasPrice: '0 Gwei',
7
+ fuelThreshold: '0 Gwei',
8
+ gasPriceEconomicalRate: 0.8,
9
+ gasPriceMinimumRate: 0.6,
10
+ gasPriceMaximumRate: 1.3,
11
+ }
12
+
13
+ if (feeDataConfig.eip1559Enabled === true) {
14
+ assert(feeDataConfig.baseFeePerGas, 'baseFeePerGas is required')
15
+ const baseFeePerGas = currency.parse(feeDataConfig.baseFeePerGas)
16
+ const gasPrice = feeDataConfig.gasPrice
17
+ ? currency.parse(feeDataConfig.gasPrice)
18
+ : baseFeePerGas.mul(1.5)
19
+ return {
20
+ gasPrice: gasPrice.toBaseString({ unit: true }),
21
+ baseFeePerGas: baseFeePerGas.toBaseString({ unit: true }),
22
+ eip1559Enabled: feeDataConfig.eip1559Enabled,
23
+ ...shared,
24
+ ...feeDataConfig,
25
+ }
26
+ }
27
+
28
+ if (feeDataConfig.eip1559Enabled === false) {
29
+ assert(feeDataConfig.gasPrice, 'gasPrice is required')
30
+ const gasPrice = currency.parse(feeDataConfig.gasPrice)
31
+ return {
32
+ gasPrice: gasPrice.toBaseString({ unit: true }),
33
+ max: gasPrice.mul(5).toBaseString({ unit: true }),
34
+ min: gasPrice.div(5).toBaseString({ unit: true }),
35
+ eip1559Enabled: feeDataConfig.eip1559Enabled,
36
+ ...shared,
37
+ ...feeDataConfig,
38
+ }
39
+ }
40
+
41
+ return {
42
+ gasPrice: '0.02 Gwei',
43
+ baseFeePerGas: '0.02 Gwei',
44
+ eip1559Enabled: true,
45
+ ...shared,
46
+ ...feeDataConfig,
47
+ }
48
+ }
49
+
50
+ export const createFeeData = ({ currency, feeDataConfig }) => {
51
+ return new FeeData({
52
+ config: createFeeDataConfigDefaults({ currency, feeDataConfig }),
53
+ mainKey: 'gasPrice',
54
+ currency,
55
+ })
56
+ }
@@ -137,12 +137,13 @@ export class EthereumNoHistoryMonitor extends BaseMonitor {
137
137
 
138
138
  for (const { tx, assetName } of pendingTransactions) {
139
139
  const txFromNode = pendingTxsFromNode[tx.txId]
140
- if (now - tx.date.getTime() > UNCONFIRMED_TX_LIMIT && txFromNode?.blockHash == null) {
140
+ const isConfirmed = Boolean(txFromNode?.blockHash)
141
+ if (now - tx.date.getTime() > UNCONFIRMED_TX_LIMIT && !isConfirmed) {
141
142
  txsToRemove.push({
142
143
  tx,
143
144
  assetSource: { asset: assetName, walletAccount },
144
145
  })
145
- } else if (txFromNode?.blockHash !== null) {
146
+ } else if (isConfirmed) {
146
147
  txsToUpdate.push({
147
148
  tx: { ...tx, confirmations: 1 },
148
149
  assetSource: { asset: assetName, walletAccount },
@@ -0,0 +1,18 @@
1
+ import { getNonce } from '../eth-like-util.js'
2
+
3
+ export const resolveNonce = async ({ asset, fromAddress, providedNonce, txLog, triedNonce }) => {
4
+ const nonceFromNode = asset.baseAsset?.api?.features?.noHistory
5
+ ? await getNonce({ asset: asset.baseAsset, address: fromAddress, tag: 'latest' }) // maybe 'pending' to unconfirmed txs
6
+ : 0
7
+
8
+ const nonceFromLog = [...txLog]
9
+ .filter((tx) => tx.sent && !tx.dropped && tx.data.nonce != null)
10
+ .reduce((nonce, tx) => Math.max(tx.data.nonce + 1, nonce), 0)
11
+
12
+ return Math.max(
13
+ nonceFromNode,
14
+ nonceFromLog,
15
+ providedNonce ?? 0,
16
+ triedNonce === undefined ? 0 : triedNonce + 1
17
+ )
18
+ }
@@ -4,9 +4,10 @@ import { calculateBumpedGasPrice, isEthereumLikeToken, normalizeTxId } from '@ex
4
4
  import assert from 'minimalistic-assert'
5
5
 
6
6
  import * as ErrorWrapper from '../error-wrapper.js'
7
- import { getNonce, transactionExists } from '../eth-like-util.js'
7
+ import { transactionExists } from '../eth-like-util.js'
8
8
  import { getNftArguments } from '../nft-utils.js'
9
9
  import getFeeInfo from './get-fee-info.js'
10
+ import { resolveNonce } from './nonce-utils.js'
10
11
 
11
12
  const txSendFactory = ({ assetClientInterface, createUnsignedTx }) => {
12
13
  assert(assetClientInterface, 'assetClientInterface is required')
@@ -21,7 +22,7 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx }) => {
21
22
  shouldLog = true,
22
23
  keepTxInput,
23
24
  txInput,
24
- nonce: _nonce,
25
+ nonce: providedNonce,
25
26
  bumpTxId,
26
27
  customFee,
27
28
  isSendAll,
@@ -56,17 +57,18 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx }) => {
56
57
  amount = asset.baseAsset.currency.ZERO
57
58
  }
58
59
 
59
- let nonceParam = _nonce
60
+ let bumpNonce
60
61
 
61
62
  let eip1559Enabled = feeData.eip1559Enabled
62
63
 
64
+ const baseAssetTxLog = await assetClientInterface.getTxLog({
65
+ assetName: baseAsset.name,
66
+ walletAccount,
67
+ })
68
+
63
69
  // `replacedTx` is always an ETH/ETC transaction (not a token)
64
70
  let replacedTx, replacedTokenTx
65
71
  if (bumpTxId) {
66
- const baseAssetTxLog = await assetClientInterface.getTxLog({
67
- assetName: baseAsset.name,
68
- walletAccount,
69
- })
70
72
  replacedTx = baseAssetTxLog.get(bumpTxId)
71
73
  if (!replacedTx || !replacedTx.pending) {
72
74
  throw new Error(`Cannot bump transaction ${bumpTxId}: not found or confirmed`)
@@ -98,21 +100,25 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx }) => {
98
100
  feeOpts.gasPrice = bumpedGasPrice
99
101
  feeOpts.tipGasPrice = bumpedTipGasPrice
100
102
  eip1559Enabled = feeData.eip1559Enabled && feeOpts.tipGasPrice
101
- nonceParam = replacedTx.data.nonce
103
+ bumpNonce = replacedTx.data.nonce
102
104
  txInput = replacedTokenTx ? null : replacedTx.data.data || '0x'
103
105
  feeAmount = feeOpts.gasPrice.mul(feeOpts.gasLimit)
104
- if (nonceParam === undefined) {
106
+ if (bumpNonce === undefined) {
105
107
  throw new Error(`Cannot bump transaction ${bumpTxId}: data object seems to be corrupted`)
106
108
  }
107
109
  }
108
110
 
111
+ const resolvedNonce =
112
+ bumpNonce ??
113
+ (await resolveNonce({ asset, fromAddress, providedNonce, txLog: baseAssetTxLog }))
114
+
109
115
  const createTxParams = {
110
116
  assetClientInterface,
111
117
  asset,
112
118
  walletAccount,
113
119
  toAddress: contractAddress || address,
114
120
  amount,
115
- nonce: nonceParam,
121
+ nonce: resolvedNonce,
116
122
  fromAddress,
117
123
  eip1559Enabled,
118
124
  customFee,
@@ -157,8 +163,13 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx }) => {
157
163
  } else if (nonceTooLowErr) {
158
164
  console.info('trying to send again...') // inject logger factory from platform
159
165
  // let's try to fix the nonce issue
160
-
161
- nonce = await getNonce({ asset: baseAsset, address: fromAddress })
166
+ nonce = await resolveNonce({
167
+ asset,
168
+ fromAddress,
169
+ providedNonce,
170
+ txLog: baseAssetTxLog,
171
+ triedNonce: nonce,
172
+ })
162
173
  ;({ txId, rawTx } = await createTx({ ...createTxParams, nonce }))
163
174
 
164
175
  try {
@@ -247,7 +258,7 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx }) => {
247
258
  })
248
259
  }
249
260
 
250
- return { txId }
261
+ return { txId, nonce }
251
262
  }
252
263
  }
253
264
 
@@ -268,6 +279,10 @@ const createTx = async ({
268
279
  feeOpts,
269
280
  createUnsignedTx,
270
281
  }) => {
282
+ assert(
283
+ nonce !== undefined && typeof nonce === 'number',
284
+ 'Nonce must be provided when creating a tx'
285
+ )
271
286
  const isToken = isEthereumLikeToken(asset)
272
287
 
273
288
  if (txInput && isToken && !keepTxInput)
@@ -309,21 +324,6 @@ const createTx = async ({
309
324
  // gasLimit = customGasLimit
310
325
  }
311
326
 
312
- if (asset.baseAsset?.api?.hasFeature?.('noHistory')) {
313
- nonce = await getNonce({ asset: asset.baseAsset, address: fromAddress })
314
- }
315
-
316
- if (nonce === undefined) {
317
- // Calculate latest nonce from base asset's TX log
318
- const baseAssetTxLog = await assetClientInterface.getTxLog({
319
- assetName: asset.baseAsset.name,
320
- walletAccount,
321
- })
322
- nonce = [...baseAssetTxLog]
323
- .filter((tx) => tx.sent && !tx.dropped && tx.data.nonce != null)
324
- .reduce((nonce, tx) => Math.max(tx.data.nonce + 1, nonce), 0)
325
- }
326
-
327
327
  const unsignedTx = await createUnsignedTx({
328
328
  asset,
329
329
  walletAccount,