@exodus/ethereum-api 8.33.0 → 8.33.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.33.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.33.1...@exodus/ethereum-api@8.33.2) (2025-03-10)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix: prevent double accounting for ethereum staking (#5184)
13
+
14
+
15
+
16
+ ## [8.33.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.33.0...@exodus/ethereum-api@8.33.1) (2025-03-06)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+
22
+ * fix: ethereum and matic staking service improvements (#5109)
23
+
24
+
25
+
6
26
  ## [8.33.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.32.0...@exodus/ethereum-api@8.33.0) (2025-03-03)
7
27
 
8
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.33.0",
3
+ "version": "8.33.2",
4
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",
@@ -64,5 +64,5 @@
64
64
  "type": "git",
65
65
  "url": "git+https://github.com/ExodusMovement/assets.git"
66
66
  },
67
- "gitHead": "15a7b5013a7364f530b88b226204cbbc769b85f0"
67
+ "gitHead": "41d13eb347b8624c223888a0e13d6709cd24f932"
68
68
  }
@@ -8,7 +8,7 @@ import { isRpcBalanceAsset } from '@exodus/ethereum-lib'
8
8
  import assert from 'minimalistic-assert'
9
9
 
10
10
  const getStaked = ({ accountState, asset }) => {
11
- return accountState?.staking?.[asset.name]?.delegatedBalance || asset.currency.ZERO
11
+ return accountState?.staking?.[asset.name]?.activeStakedBalance || asset.currency.ZERO
12
12
  }
13
13
 
14
14
  const getStaking = ({ accountState, asset }) => {
@@ -134,8 +134,10 @@ const getFeeAsyncFactory = ({
134
134
  const l1DataFee = optimismL1DataFee
135
135
  ? asset.baseAsset.currency.baseUnit(optimismL1DataFee)
136
136
  : asset.baseAsset.currency.ZERO
137
+
137
138
  return {
138
139
  fee: fee.add(l1DataFee),
140
+ // TODO: Should this be `l1DataFee`?
139
141
  optimismL1DataFee,
140
142
  gasLimit,
141
143
  gasPrice,
package/src/get-fee.js CHANGED
@@ -34,12 +34,16 @@ export const getFeeFactory =
34
34
  isRbfAllowed = true, // Destkop, isRbfAllowed=true when advanced panel is on
35
35
  calculateEffectiveFee,
36
36
  }) => {
37
- const { eip1559Enabled, baseFeePerGas, tipGasPrice } = feeData
37
+ const { eip1559Enabled, baseFeePerGas, tipGasPrice, useBaseGasPrice } = feeData
38
38
 
39
39
  const gasPrice = customFee || resolveGasPrice({ feeData })
40
40
 
41
41
  const gasLimit = providedGasLimit || asset.gasLimit || defaultGasLimit
42
42
 
43
+ // When explicitly opting into EIP-1559 transactions,
44
+ // lock in the `tipGasPrice` we used to compute the fees.
45
+ const maybeReturnTipGasPrice = eip1559Enabled && useBaseGasPrice ? { tipGasPrice } : null
46
+
43
47
  const extraFeeData = getExtraFeeData({ asset, amount })
44
48
  if (calculateEffectiveFee && eip1559Enabled) {
45
49
  const maxFeePerGas = gasPrice
@@ -47,11 +51,16 @@ export const getFeeFactory =
47
51
  const feePerGas = baseFeePerGas.add(tipGasPrice)
48
52
  const effectiveGasPrice = feePerGas.lt(maxFeePerGas) ? feePerGas : maxFeePerGas
49
53
 
50
- return { fee: effectiveGasPrice.mul(gasLimit), gasPrice, extraFeeData }
54
+ return {
55
+ ...maybeReturnTipGasPrice,
56
+ fee: effectiveGasPrice.mul(gasLimit),
57
+ gasPrice,
58
+ extraFeeData,
59
+ }
51
60
  }
52
61
 
53
62
  const fee = gasPrice.mul(gasLimit)
54
- return { fee, gasPrice, extraFeeData }
63
+ return { ...maybeReturnTipGasPrice, fee, gasPrice, extraFeeData }
55
64
  }
56
65
 
57
66
  // Used in BE
@@ -28,6 +28,14 @@ export class EthereumStaking {
28
28
  UNSTAKE: '0x76ec871c', // unstake(uint256 amount)
29
29
  UNSTAKE_PENDING: '0xed0723d4', // unstakePending(uint256 amount)
30
30
  CLAIM_UNSTAKE: '0x33986ffa', // claimWithdrawRequest(uint256 amount)
31
+ DEPOSIT: '0x47e7ef24', // deposit(address staker, uint256 amount)
32
+ }
33
+
34
+ static isDelegationTransactonCalldata = (txInput) => {
35
+ const txInputHex = (Buffer.isBuffer(txInput) ? bufferToHex(txInput) : txInput) || '0x'
36
+ if (typeof txInputHex !== 'string') throw new Error('expected string')
37
+
38
+ return txInputHex.toLowerCase().startsWith(EthereumStaking.METHODS_IDS.DELEGATE)
31
39
  }
32
40
 
33
41
  constructor(
@@ -52,7 +60,10 @@ export class EthereumStaking {
52
60
  return contract[method].build(...args)
53
61
  }
54
62
 
55
- #callReadFunctionContract = (contract, method, ...args) => {
63
+ #callReadFunctionContract = (contract, method, ...args) =>
64
+ this.#callReadFunctionContractFrom(contract, method, undefined, ...args)
65
+
66
+ #callReadFunctionContractFrom = (contract, method, from, ...args) => {
56
67
  const callData = this.#buildTxData(contract, method, ...args)
57
68
  const data = {
58
69
  data: bufferToHex(callData),
@@ -60,6 +71,8 @@ export class EthereumStaking {
60
71
  tag: 'latest',
61
72
  }
62
73
 
74
+ if (typeof from === 'string' && from.length > 0) data.from = from
75
+
63
76
  const eth = this.server || getServerByName(this.asset.name)
64
77
  return retry((...args) => eth.ethCall(...args), { delayTimesMs: RETRY_DELAYS })(data)
65
78
  }
@@ -151,6 +164,25 @@ export class EthereumStaking {
151
164
 
152
165
  // === POOL ===
153
166
 
167
+ /** Determine the effects of a deposit operation by mocking a transaction from the Everstake Pool contract, which can be used to estimate slippage. */
168
+ async getDepositEffects(depositorAddress, amount) {
169
+ const { address: from } = this.contractPool
170
+ const prankDepositReturnData = await this.#callReadFunctionContractFrom(
171
+ this.contractAccounting,
172
+ 'deposit',
173
+ from,
174
+ depositorAddress,
175
+ amount.toBaseString()
176
+ )
177
+ const [interchangedAmount, activatedSlots] =
178
+ this.contractAccounting.deposit.parse(prankDepositReturnData)
179
+
180
+ return {
181
+ interchangedAmount: this.asset.currency.baseUnit(interchangedAmount),
182
+ activatedSlots: BigInt(activatedSlots),
183
+ }
184
+ }
185
+
154
186
  async stake({ amount, source = '2' }) {
155
187
  if (amount.gte(this.minAmount)) {
156
188
  return {
@@ -1,13 +1,35 @@
1
- import { isNumberUnit } from '@exodus/currency'
2
-
3
- import { getServer } from '../../exodus-eth-server/index.js'
4
1
  import { estimateGasLimit } from '../../gas-estimation.js'
5
- import { fromHexToBigInt } from '../../number-utils.js'
6
2
  import { createWatchTx as defaultCreateWatch } from '../../watch-tx.js'
7
3
  import { stakingProviderClientFactory } from '../staking-provider-client.js'
4
+ import { amountToCurrency, getEvmStakingServiceFee } from '../utils/index.js'
8
5
  import { EthereumStaking } from './api.js'
9
6
 
10
- const extraGasLimit = 20_000 // extra gas Limit to prevent tx failing if something change on pool state (till tx is in mempool)
7
+ const WETH9_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
8
+
9
+ // Transactions will usually stake values up to `1 ether`, so
10
+ // we'll assume this amount as a slippage protection mechanism,
11
+ // the idea being that this is a value of ether that we risk
12
+ // being mined whilst our transaction is in-flight.
13
+ const SLOT_ACTIVATION_PROXIMITY_ETH = '1.0'
14
+
15
+ // We've observed that most transactions which offer
16
+ // a healthy ratio between `gasUsed` had a `gasLimit`
17
+ // over `170_000`. Transactions which have an unhealthy
18
+ // ratio commonly fall under this limit.
19
+ const MINIMUM_DELEGATION_GAS_LIMIT = 180_000
20
+
21
+ // Use a custom `maxPriorityFeePerGas` so we can
22
+ // respect the default EIP-1559 relationship:
23
+ // maxFeePerGas = baseFeePerGas + maxPriorityFeePerGas.
24
+ //
25
+ // Currently, the backend will return an excessive
26
+ // `tipGasPrice` which hinders the configuration ofrational
27
+ // inclusion incentives when defined using remote config.
28
+ const MAX_PRIORITY_FEE_PER_GAS = '0.06 Gwei' // semi-urgent
29
+
30
+ const EXTRA_GAS_LIMIT = 20_000 // extra gas Limit to prevent tx failing if something change on pool state (till tx is in mempool)
31
+
32
+ const DISABLE_BALANCE_CHECKS = '0x0'
11
33
 
12
34
  export function createEthereumStakingService({
13
35
  asset,
@@ -18,10 +40,6 @@ export function createEthereumStakingService({
18
40
  const stakingProvider = stakingProviderClientFactory()
19
41
  const minAmount = staking.minAmount
20
42
 
21
- function amountToCurrency({ asset, amount }) {
22
- return isNumberUnit(amount) ? amount : asset.currency.parse(amount)
23
- }
24
-
25
43
  async function delegate({ walletAccount, amount } = Object.create(null)) {
26
44
  const address = await assetClientInterface.getReceiveAddress({
27
45
  assetName: asset.name,
@@ -34,13 +52,18 @@ export function createEthereumStakingService({
34
52
  amount,
35
53
  })
36
54
 
37
- const { gasPrice, gasLimit } = await estimateTxFee(delegatorAddress, to, amount, data)
38
-
39
55
  console.log(
40
56
  `delegator address ${delegatorAddress} staking ${amount.toDefaultString({
41
57
  unit: true,
42
58
  })}`
43
59
  )
60
+
61
+ const { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee(
62
+ delegatorAddress,
63
+ to,
64
+ amount,
65
+ data
66
+ )
44
67
  const txId = await prepareAndSendTx({
45
68
  asset,
46
69
  walletAccount,
@@ -49,6 +72,7 @@ export function createEthereumStakingService({
49
72
  txData: data,
50
73
  gasPrice,
51
74
  gasLimit,
75
+ tipGasPrice,
52
76
  })
53
77
 
54
78
  // Goerli is not supported
@@ -83,9 +107,14 @@ export function createEthereumStakingService({
83
107
  amount: inactiveAmountToUnstake,
84
108
  })
85
109
 
86
- const feeData = await estimateTxFee(delegatorAddress, to, null, data)
110
+ const { gasLimit, gasPrice, tipGasPrice } = await estimateTxFee(
111
+ delegatorAddress,
112
+ to,
113
+ null,
114
+ data
115
+ )
87
116
 
88
- return { to, txData: data, ...feeData }
117
+ return { to, txData: data, gasLimit, gasPrice, tipGasPrice }
89
118
  }
90
119
 
91
120
  async function getUndelegateData({ delegatorAddress, resquestedAmount, pendingAmount }) {
@@ -102,9 +131,13 @@ export function createEthereumStakingService({
102
131
  amount: activeAmountToUnstake,
103
132
  })
104
133
 
105
- const feeData = await estimateTxFee(delegatorAddress, to, null, data)
106
-
107
- return { to, txData: data, ...feeData }
134
+ const { gasLimit, gasPrice, tipGasPrice } = await estimateTxFee(
135
+ delegatorAddress,
136
+ to,
137
+ null,
138
+ data
139
+ )
140
+ return { to, txData: data, gasLimit, gasPrice, tipGasPrice }
108
141
  }
109
142
 
110
143
  /**
@@ -226,14 +259,21 @@ export function createEthereumStakingService({
226
259
  })
227
260
  if (withdrawRequest) {
228
261
  const { to, data } = withdrawRequest
229
- const { gasPrice, gasLimit } = await estimateTxFee(delegatorAddress, to, null, data)
262
+
263
+ const { gasLimit, gasPrice, tipGasPrice } = await estimateTxFee(
264
+ delegatorAddress,
265
+ to,
266
+ null,
267
+ data
268
+ )
230
269
  return prepareAndSendTx({
231
270
  asset,
232
271
  walletAccount,
233
272
  to,
234
273
  txData: data,
235
- gasPrice,
236
274
  gasLimit,
275
+ gasPrice,
276
+ tipGasPrice,
237
277
  })
238
278
  }
239
279
  }
@@ -266,44 +306,113 @@ export function createEthereumStakingService({
266
306
  }
267
307
 
268
308
  const { amount, data } = await delegateOperation({ ...args, amount: requestedAmount })
269
- const toAddress =
270
- operation === 'claimUndelegatedBalance' ? staking.accountingAddress : staking.poolAddress
271
309
 
272
- const { fee } = await estimateTxFee(delegatorAddress, toAddress, amount, data)
310
+ const { fee } = await (operation === 'claimUndelegatedBalance'
311
+ ? estimateTxFee(delegatorAddress, staking.accountingAddress, amount, data)
312
+ : // The `gasUsed` of a delegation transaction can vary
313
+ // significantly depending upon whether it will result
314
+ // in the activation of new slots (i.e. validator creation).
315
+ //
316
+ // This can result in transaction `revert` due to slippage
317
+ // from incompatible transaction ordering.
318
+ //
319
+ // To mitigate this, we:
320
+ // 1. Simulate the delegation at an amplified deposit amount
321
+ // to show enhanced fees close to a proximity buffer.
322
+ // 2. Originate the transaction from the WETH contract,
323
+ // which guarantees deep native ether liquidity which
324
+ // exceeds any rational user deposit.
325
+ estimateTxFee(
326
+ WETH9_ADDRESS,
327
+ staking.poolAddress,
328
+ amount.add(asset.currency.defaultUnit(SLOT_ACTIVATION_PROXIMITY_ETH)),
329
+ data
330
+ ))
273
331
 
274
332
  return fee
275
333
  }
276
334
 
277
- async function estimateTxFee(from, to, amount, txInput, gasPrice = '0x0') {
335
+ async function estimateTxFee(from, to, amount, txInput) {
278
336
  amount = amount || asset.currency.ZERO
337
+ from = from.toLowerCase()
279
338
 
280
- const gasLimit = await estimateGasLimit(
339
+ const estimatedGasLimit = await estimateGasLimit(
281
340
  asset,
282
- from.toLowerCase(),
341
+ from,
283
342
  to.toLowerCase(),
284
343
  amount, // staking contracts does not always require ETH amount to interact with
285
344
  txInput,
286
- gasPrice
345
+ DISABLE_BALANCE_CHECKS
287
346
  )
288
347
 
289
- if (gasPrice === '0x0') {
290
- gasPrice = await getServer(asset).gasPrice()
291
- }
348
+ const gasLimit = Math.max(
349
+ estimatedGasLimit + EXTRA_GAS_LIMIT,
292
350
 
293
- gasPrice = fromHexToBigInt(gasPrice)
294
- const fee = gasPrice * BigInt(gasLimit + extraGasLimit)
351
+ // For delgation transactions, we enforce an empirical
352
+ // `MINIMUM_DELEGATE_GAS_LIMIT`, since the majority of
353
+ // transactions which possess a healthy ratio of
354
+ // `gasUsed` to `gasLimit` operate at this boundary.
355
+ EthereumStaking.isDelegationTransactonCalldata(txInput) ? MINIMUM_DELEGATION_GAS_LIMIT : 0
356
+ )
295
357
 
296
- return {
358
+ return getEvmStakingServiceFee({
359
+ amount,
360
+ asset,
361
+ assetClientInterface,
362
+ maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS,
297
363
  gasLimit,
298
- gasPrice: asset.currency.baseUnit(gasPrice.toString()),
299
- fee: asset.currency.baseUnit(fee.toString()),
300
- }
364
+ })
301
365
  }
302
366
 
367
+ /** Returns the minimum possible amount that can be staked. */
303
368
  function getMinAmount() {
304
369
  return staking.minAmount
305
370
  }
306
371
 
372
+ /** Determine the maximum possible stake for a given spendable amount. */
373
+ async function getDelegateSelectAllAmount({
374
+ walletAccount,
375
+ spendableForStaking = asset.currency.ZERO,
376
+ }) {
377
+ const minAmount = getMinAmount()
378
+
379
+ // If the caller hasn't specified a value of `spendableForStaking`
380
+ // which satisfies the minimum amount, then we'll coerce this value
381
+ // up to the minimum amount so we can at least provide a rational
382
+ // estimate for the `calculatedFee`, so they know how much ether
383
+ // to purchase.
384
+ spendableForStaking = spendableForStaking.lt(minAmount) ? minAmount : spendableForStaking
385
+
386
+ // Compute the cost of delegation. Even though the `walletAccount`'s
387
+ // balance is potentially insufficient, we can still provide
388
+ // rational fee estimates because the estimation process will mock
389
+ // the transaction from an account with deep liquidity.
390
+ const calculatedFee = await estimateDelegateOperation({
391
+ walletAccount,
392
+ operation: 'delegate',
393
+ args: {
394
+ amount: spendableForStaking,
395
+ },
396
+ })
397
+
398
+ // If the `spendableForStaking` is insufficient to cover both the
399
+ // transaction fee and the minimum stake, then there is no reasonable
400
+ // value we can recommend.
401
+ if (calculatedFee.add(minAmount).gt(spendableForStaking))
402
+ return {
403
+ calculatedFee,
404
+ selectAllAmount: asset.currency.ZERO,
405
+ }
406
+
407
+ // At this stage, we've confirmed that the remaining `spendableForStaking`
408
+ // after transactions is sufficient to cover the minimum stake, so any
409
+ // excess amounts over this can be.
410
+ return {
411
+ calculatedFee,
412
+ selectAllAmount: spendableForStaking.sub(calculatedFee),
413
+ }
414
+ }
415
+
307
416
  async function prepareAndSendTx(
308
417
  {
309
418
  asset,
@@ -313,6 +422,7 @@ export function createEthereumStakingService({
313
422
  txData: txInput,
314
423
  gasPrice,
315
424
  gasLimit,
425
+ tipGasPrice,
316
426
  waitForConfirmation = false,
317
427
  } = Object.create(null)
318
428
  ) {
@@ -327,6 +437,8 @@ export function createEthereumStakingService({
327
437
  txInput,
328
438
  gasPrice,
329
439
  gasLimit,
440
+ // HACK: Override the `tipGasPrice` to use a custom `maxPriorityFeePerGas`.
441
+ tipGasPrice,
330
442
  },
331
443
  }
332
444
 
@@ -353,6 +465,7 @@ export function createEthereumStakingService({
353
465
  getEthereumStakingInfo,
354
466
  estimateDelegateOperation,
355
467
  getMinAmount,
468
+ getDelegateSelectAllAmount,
356
469
  }
357
470
  }
358
471
 
@@ -1,12 +1,15 @@
1
- import { isNumberUnit } from '@exodus/currency'
2
1
  import BN from 'bn.js'
3
2
 
4
3
  import { getServer } from '../../exodus-eth-server/index.js'
5
4
  import { estimateGasLimit } from '../../gas-estimation.js'
6
5
  import { createWatchTx as defaultCreateWatch } from '../../watch-tx.js'
7
6
  import { stakingProviderClientFactory } from '../staking-provider-client.js'
7
+ import { amountToCurrency, getEvmStakingServiceFee } from '../utils/index.js'
8
8
  import { MaticStakingApi } from './api.js'
9
9
 
10
+ const DISABLE_BALANCE_CHECKS = '0x0'
11
+ const MAX_PRIORITY_FEE_PER_GAS = '0.06 Gwei' // semi-urgent
12
+
10
13
  export function createPolygonStakingService({
11
14
  assetClientInterface,
12
15
  createWatchTx = defaultCreateWatch,
@@ -22,10 +25,6 @@ export function createPolygonStakingService({
22
25
  return { asset, feeAsset }
23
26
  }
24
27
 
25
- function amountToCurrency({ asset, amount }) {
26
- return isNumberUnit(amount) ? amount : asset.currency.parse(amount)
27
- }
28
-
29
28
  async function delegate({ walletAccount, amount } = {}) {
30
29
  const address = await assetClientInterface.getReceiveAddress({
31
30
  assetName,
@@ -37,7 +36,7 @@ export function createPolygonStakingService({
37
36
  amount = amountToCurrency({ asset, amount })
38
37
 
39
38
  const txApproveData = await stakingApi.approveStakeManager(amount)
40
- let { gasPrice, gasLimit } = await estimateTxFee(
39
+ let { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee(
41
40
  delegatorAddress,
42
41
  stakingApi.polygonContract.address,
43
42
  txApproveData
@@ -49,10 +48,12 @@ export function createPolygonStakingService({
49
48
  txData: txApproveData,
50
49
  gasPrice,
51
50
  gasLimit,
51
+ tipGasPrice,
52
52
  })
53
53
 
54
54
  const txDelegateData = await stakingApi.delegate({ amount })
55
- ;({ gasPrice, gasLimit } = await estimateTxFee(
55
+
56
+ ;({ gasPrice, gasLimit, tipGasPrice } = await estimateTxFee(
56
57
  delegatorAddress,
57
58
  stakingApi.validatorShareContract.address,
58
59
  txDelegateData
@@ -64,6 +65,7 @@ export function createPolygonStakingService({
64
65
  txData: txDelegateData,
65
66
  gasPrice,
66
67
  gasLimit,
68
+ tipGasPrice,
67
69
  })
68
70
 
69
71
  await stakingProvider.notifyStaking({
@@ -87,7 +89,7 @@ export function createPolygonStakingService({
87
89
  amount = amountToCurrency({ asset, amount })
88
90
 
89
91
  const txUndelegateData = await stakingApi.undelegate({ amount })
90
- const { gasPrice, gasLimit } = await estimateTxFee(
92
+ const { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee(
91
93
  delegatorAddress.toLowerCase(),
92
94
  stakingApi.validatorShareContract.address,
93
95
  txUndelegateData
@@ -98,6 +100,7 @@ export function createPolygonStakingService({
98
100
  txData: txUndelegateData,
99
101
  gasPrice,
100
102
  gasLimit,
103
+ tipGasPrice,
101
104
  })
102
105
  }
103
106
 
@@ -109,7 +112,7 @@ export function createPolygonStakingService({
109
112
  const delegatorAddress = address.toLowerCase()
110
113
 
111
114
  const txWithdrawRewardsData = await stakingApi.withdrawRewards()
112
- const { gasPrice, gasLimit } = await estimateTxFee(
115
+ const { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee(
113
116
  delegatorAddress,
114
117
  stakingApi.validatorShareContract.address,
115
118
  txWithdrawRewardsData
@@ -120,6 +123,7 @@ export function createPolygonStakingService({
120
123
  txData: txWithdrawRewardsData,
121
124
  gasPrice,
122
125
  gasLimit,
126
+ tipGasPrice,
123
127
  })
124
128
  }
125
129
 
@@ -145,7 +149,7 @@ export function createPolygonStakingService({
145
149
  })
146
150
 
147
151
  const txClaimUndelegatedData = await stakingApi.claimUndelegatedBalance({ unbondNonce })
148
- const { gasPrice, gasLimit, fee } = await estimateTxFee(
152
+ const { gasPrice, gasLimit, tipGasPrice, fee } = await estimateTxFee(
149
153
  delegatorAddress,
150
154
  stakingApi.validatorShareContract.address,
151
155
  txClaimUndelegatedData
@@ -156,6 +160,7 @@ export function createPolygonStakingService({
156
160
  txData: txClaimUndelegatedData,
157
161
  gasPrice,
158
162
  gasLimit,
163
+ tipGasPrice,
159
164
  fee,
160
165
  })
161
166
 
@@ -233,32 +238,29 @@ export function createPolygonStakingService({
233
238
  }
234
239
  }
235
240
 
236
- async function estimateTxFee(from, to, txInput, gasPrice = '0x0') {
241
+ async function estimateTxFee(from, to, txInput) {
237
242
  const { ethereum } = await assetClientInterface.getAssetsForNetwork({
238
243
  baseAssetName: 'ethereum',
239
244
  })
245
+
240
246
  const amount = ethereum.currency.ZERO
247
+
241
248
  const gasLimit = await estimateGasLimit(
242
249
  ethereum,
243
250
  from,
244
251
  to,
245
252
  amount, // staking contracts does not require ETH amount to interact with
246
253
  txInput,
247
- gasPrice
254
+ DISABLE_BALANCE_CHECKS
248
255
  )
249
256
 
250
- if (gasPrice === '0x0') {
251
- gasPrice = await getServer(ethereum).gasPrice()
252
- }
253
-
254
- gasPrice = parseInt(gasPrice, 16)
255
- const fee = new BN(gasPrice).mul(new BN(gasLimit))
256
-
257
- return {
257
+ return getEvmStakingServiceFee({
258
+ amount,
259
+ asset: ethereum,
260
+ assetClientInterface,
261
+ maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS,
258
262
  gasLimit,
259
- gasPrice: ethereum.currency.baseUnit(gasPrice),
260
- fee: ethereum.currency.baseUnit(fee.toString()),
261
- }
263
+ })
262
264
  }
263
265
 
264
266
  async function prepareAndSendTx({
@@ -267,6 +269,7 @@ export function createPolygonStakingService({
267
269
  txData: txInput,
268
270
  gasPrice,
269
271
  gasLimit,
272
+ tipGasPrice,
270
273
  waitForConfirmation = false,
271
274
  } = {}) {
272
275
  const { ethereum: asset } = await assetClientInterface.getAssetsForNetwork({
@@ -282,6 +285,7 @@ export function createPolygonStakingService({
282
285
  txInput,
283
286
  gasPrice,
284
287
  gasLimit,
288
+ tipGasPrice,
285
289
  },
286
290
  waitForConfirmation,
287
291
  }
@@ -0,0 +1,73 @@
1
+ import { isNumberUnit } from '@exodus/currency'
2
+
3
+ export function amountToCurrency({ asset, amount }) {
4
+ return isNumberUnit(amount) ? amount : asset.currency.parse(amount)
5
+ }
6
+
7
+ // HACK: Empirically, we can observe that a `feeData` object uses
8
+ // stringified values for gas i.e. `baseFeePerGas: "10 gwei"`,
9
+ // however to hook into `asset.api.getFees()`, these values
10
+ // must be expressed using currency objects.
11
+ function maybeNormalizeFeeData({ asset, feeData }) {
12
+ const { baseFeePerGas, tipGasPrice, serverGasPrice, gasPrice, ...extras } = feeData
13
+
14
+ const maybeNormalizeAmount = (amount) =>
15
+ typeof amount === 'string' ? amountToCurrency({ amount, asset }) : amount
16
+
17
+ return {
18
+ ...extras,
19
+ gasPrice: maybeNormalizeAmount(gasPrice),
20
+ baseFeePerGas: maybeNormalizeAmount(baseFeePerGas),
21
+ tipGasPrice: maybeNormalizeAmount(tipGasPrice),
22
+ serverGasPrice: maybeNormalizeAmount(serverGasPrice),
23
+ }
24
+ }
25
+
26
+ /**
27
+ * A common handler for the computation of the `fee`, `gasPrice`,
28
+ * `gasLimit` and `tipGasPrice` or a staking call. Allows the caller
29
+ * to specify a custom `maxPriorityFeePerGas` whilst the current
30
+ * `tipGasPrice` is incompatible with EIP-1559 pricing.
31
+ */
32
+ export async function getEvmStakingServiceFee({
33
+ amount,
34
+ asset,
35
+ assetClientInterface,
36
+ maxPriorityFeePerGas: maybeMaxPriorityFeePerGas,
37
+ gasLimit,
38
+ }) {
39
+ const defaultFeeData = await assetClientInterface.getFeeData({
40
+ assetName: asset.name,
41
+ })
42
+
43
+ // HACK: We wish to explicitly enable `useBaseGasPrice`
44
+ // since we're using a custom `tipGasPrice`. This
45
+ // will be compatible during the transition from
46
+ // Magnifier to Clarity, and can be safely removed
47
+ // once `useBaseGasPrice` is the default behaviour
48
+ // (alongside the custom tip).
49
+ const { eip1559Enabled } = defaultFeeData
50
+ const useBaseGasPrice = Boolean(eip1559Enabled) && Boolean(maybeMaxPriorityFeePerGas)
51
+
52
+ const normalizedFeeData = maybeNormalizeFeeData({
53
+ asset,
54
+ feeData: {
55
+ ...defaultFeeData,
56
+ // HACK: The backend currently exports a very large `tipGasPrice` that is
57
+ // compatible with Magnifier's legacy `gasPrice`, but it would
58
+ // be incompatible with EIP-1559, since this would evaluate into
59
+ // a very large `maxPriorityFeePerGas`.
60
+ ...(useBaseGasPrice ? { tipGasPrice: maybeMaxPriorityFeePerGas } : null),
61
+ useBaseGasPrice,
62
+ },
63
+ })
64
+
65
+ // Returns `fee`, `gasPrice`, `extraFeeData`, and `tipGasPrice`
66
+ // if the config defines we should `useBaseGasPrice`.
67
+ return asset.api.getFee({
68
+ asset,
69
+ feeData: normalizedFeeData,
70
+ gasLimit,
71
+ amount,
72
+ })
73
+ }
@@ -35,6 +35,7 @@ export default function getLogItemsFromServerTx({
35
35
  const methodId = serverTx.input && {
36
36
  methodId: serverTx.input.slice(0, Math.max(0, METHOD_ID_LENGTH)),
37
37
  }
38
+ const data = serverTx.input || '0x'
38
39
 
39
40
  const logItemCommonProperties = {
40
41
  confirmations,
@@ -77,7 +78,7 @@ export default function getLogItemsFromServerTx({
77
78
  coinAmount,
78
79
  coinName: asset.name,
79
80
  data: {
80
- data: serverTx.input || '0x',
81
+ data,
81
82
  nonce,
82
83
  gasLimit,
83
84
  ...methodId,
@@ -137,7 +138,7 @@ export default function getLogItemsFromServerTx({
137
138
  ...logItemCommonProperties,
138
139
  coinAmount,
139
140
  coinName: tokenName,
140
- data: { nonce, gasLimit },
141
+ data: { data, nonce, gasLimit, ...methodId },
141
142
  ...(isConsideredSent
142
143
  ? { from: [], to: tokenTransferToAddress, feeAmount, feeCoinName: token.feeAsset.name }
143
144
  : { from: tokenFromAddresses }),