@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 +20 -0
- package/package.json +2 -2
- package/src/get-balances.js +1 -1
- package/src/get-fee-async.js +2 -0
- package/src/get-fee.js +12 -3
- package/src/staking/ethereum/api.js +33 -1
- package/src/staking/ethereum/service.js +147 -34
- package/src/staking/matic/service.js +27 -23
- package/src/staking/utils/index.js +73 -0
- package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +3 -2
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.
|
|
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": "
|
|
67
|
+
"gitHead": "41d13eb347b8624c223888a0e13d6709cd24f932"
|
|
68
68
|
}
|
package/src/get-balances.js
CHANGED
|
@@ -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]?.
|
|
11
|
+
return accountState?.staking?.[asset.name]?.activeStakedBalance || asset.currency.ZERO
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const getStaking = ({ accountState, asset }) => {
|
package/src/get-fee-async.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
110
|
+
const { gasLimit, gasPrice, tipGasPrice } = await estimateTxFee(
|
|
111
|
+
delegatorAddress,
|
|
112
|
+
to,
|
|
113
|
+
null,
|
|
114
|
+
data
|
|
115
|
+
)
|
|
87
116
|
|
|
88
|
-
return { to, txData: data,
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
335
|
+
async function estimateTxFee(from, to, amount, txInput) {
|
|
278
336
|
amount = amount || asset.currency.ZERO
|
|
337
|
+
from = from.toLowerCase()
|
|
279
338
|
|
|
280
|
-
const
|
|
339
|
+
const estimatedGasLimit = await estimateGasLimit(
|
|
281
340
|
asset,
|
|
282
|
-
from
|
|
341
|
+
from,
|
|
283
342
|
to.toLowerCase(),
|
|
284
343
|
amount, // staking contracts does not always require ETH amount to interact with
|
|
285
344
|
txInput,
|
|
286
|
-
|
|
345
|
+
DISABLE_BALANCE_CHECKS
|
|
287
346
|
)
|
|
288
347
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
348
|
+
const gasLimit = Math.max(
|
|
349
|
+
estimatedGasLimit + EXTRA_GAS_LIMIT,
|
|
292
350
|
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
254
|
+
DISABLE_BALANCE_CHECKS
|
|
248
255
|
)
|
|
249
256
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
|
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 }),
|