@exodus/ethereum-plugin 2.23.0 → 2.23.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/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
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
+ ## [2.23.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-plugin@2.23.0...@exodus/ethereum-plugin@2.23.1) (2026-02-09)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix: monitors should check rpc before dropping (#7277)
13
+
14
+
15
+
6
16
  ## [2.23.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-plugin@2.22.0...@exodus/ethereum-plugin@2.23.0) (2026-01-14)
7
17
 
8
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-plugin",
3
- "version": "2.23.0",
3
+ "version": "2.23.1",
4
4
  "description": "Ethereum plugin for Exodus SDK powered wallets",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -21,8 +21,9 @@
21
21
  "lint:fix": "yarn lint --fix"
22
22
  },
23
23
  "dependencies": {
24
+ "@exodus/basic-utils": "^3.0.1",
24
25
  "@exodus/currency": "^6.0.1",
25
- "@exodus/ethereum-api": "^8.64.0",
26
+ "@exodus/ethereum-api": "^8.64.4",
26
27
  "@exodus/ethereum-lib": "^5.21.0",
27
28
  "@exodus/ethereum-meta": "^2.9.0",
28
29
  "@exodus/ethereumjs": "^1.0.0",
@@ -33,11 +34,10 @@
33
34
  "devDependencies": {
34
35
  "@exodus/assets": "^11.4.0",
35
36
  "@exodus/assets-testing": "^1.0.0",
36
- "@exodus/basic-utils": "^3.0.1",
37
37
  "@exodus/crypto": "^1.0.0-rc.13",
38
38
  "@exodus/evm-fork-testing": "^0.4.0",
39
39
  "@exodus/models": "^12.13.0",
40
- "@exodus/web3-ethereum-utils": "^4.7.1",
40
+ "@exodus/web3-ethereum-utils": "^4.7.2",
41
41
  "delay": "^4.0.1",
42
42
  "ms": "^2.1.1"
43
43
  },
@@ -49,5 +49,5 @@
49
49
  "type": "git",
50
50
  "url": "git+https://github.com/ExodusMovement/assets.git"
51
51
  },
52
- "gitHead": "11f40d9da07e1e70a4e07f94defa3042c6fb21cc"
52
+ "gitHead": "ebcc3fe022dfc1e21e3ccdc00faf0769e8fb3092"
53
53
  }
@@ -115,6 +115,30 @@ class StakingServer {
115
115
  return this.asset.currency.baseUnit(liquidRewards)
116
116
  }
117
117
 
118
+ getTotalRewards = async (address) => {
119
+ // use external everstake API to get total rewards
120
+ const EVERSTAKE_API_URL = 'https://polygon-clarity.a.exodus.io/everstake-rewards'
121
+ const url = new URL(EVERSTAKE_API_URL)
122
+
123
+ const options = {
124
+ method: 'POST',
125
+ headers: { 'Content-Type': 'application/json' },
126
+ body: JSON.stringify({
127
+ address,
128
+ }),
129
+ }
130
+
131
+ try {
132
+ const response = await fetch(url.toString(), options)
133
+ const res = await response.json()
134
+ if (res === 'address' || !res?.rewards) return this.asset.currency.ZERO // address not found
135
+ return this.asset.currency.baseUnit(res.rewards)
136
+ } catch (error) {
137
+ console.warn('Error fetching Polygon total rewards:', error)
138
+ return this.asset.currency.ZERO
139
+ }
140
+ }
141
+
118
142
  getTotalStake = async (address) => {
119
143
  const stakeInfo = await this.#callReadFunctionContract(
120
144
  this.validatorShareContract,
@@ -182,5 +206,5 @@ class StakingServer {
182
206
  }
183
207
  }
184
208
 
185
- export const stakingServerFactory = ({ asset, contracts, server }) =>
186
- new StakingServer({ asset, contracts, server })
209
+ export const stakingServerFactory = ({ assetName, currency, contracts, server }) =>
210
+ new StakingServer({ asset: { name: assetName, currency }, contracts, server })
@@ -0,0 +1,107 @@
1
+ import NumberUnit from '@exodus/currency'
2
+ import { parseUnsignedTx } from '@exodus/ethereum-lib'
3
+ import assert from 'minimalistic-assert'
4
+
5
+ // Build simulation transaction requests from unsigned transactions
6
+ export function createMaticStakeSimRequestsFromUnsigned({
7
+ asset,
8
+ unsignedTxApprove,
9
+ unsignedTxDelegate,
10
+ }) {
11
+ const build = (tx) => {
12
+ const parsed = parseUnsignedTx({ asset, unsignedTx: tx.unsignedTx })
13
+ const gasPriceUnit = tx.gasPrice
14
+ const chainId = tx.unsignedTx?.txData?.chainId
15
+ const from = tx.unsignedTx?.txMeta?.fromAddress
16
+ const to = tx.unsignedTx?.txMeta?.toAddress
17
+
18
+ assert(gasPriceUnit instanceof NumberUnit, 'expected NumberUnit gasPrice')
19
+ assert(Number.isInteger(chainId), 'expected integer chainId')
20
+ assert(typeof from === 'string' && from, 'expected string fromAddress')
21
+ assert(typeof to === 'string' && to, 'expected string toAddress')
22
+
23
+ return {
24
+ chainId,
25
+ from,
26
+ to,
27
+ gas: `0x${parsed.gasLimit.toString(16)}`,
28
+ gasPrice: `0x${BigInt(gasPriceUnit.toBaseString()).toString(16)}`,
29
+ value: '0x0',
30
+ data: `0x${parsed.data?.toString('hex')}`,
31
+ nonce: `0x${parsed.nonce.toString(16)}`,
32
+ }
33
+ }
34
+
35
+ return {
36
+ approveTxRequest: build(unsignedTxApprove),
37
+ delegateTxRequest: build(unsignedTxDelegate),
38
+ }
39
+ }
40
+
41
+ export async function maticDelegateSimulateTransactions({
42
+ asset,
43
+ unsignedTxApprove,
44
+ unsignedTxDelegate,
45
+ senderAddress,
46
+ revertOnSimulationError,
47
+ }) {
48
+ const { approveTxRequest, delegateTxRequest } = createMaticStakeSimRequestsFromUnsigned({
49
+ asset: asset.baseAsset,
50
+ unsignedTxApprove,
51
+ unsignedTxDelegate,
52
+ })
53
+
54
+ // Simulate transactions
55
+ const simulationTxData = {
56
+ baseAssetName: asset.baseAsset.name,
57
+ origin: 'exodus-staking',
58
+ senderAddress,
59
+ }
60
+
61
+ const simulationResponse = await asset.baseAsset.api.web3.simulateTransactions({
62
+ ...simulationTxData,
63
+ transactions: [approveTxRequest, delegateTxRequest],
64
+ })
65
+ // These checks are only for simulation issues, not validation of the transactions
66
+ if (simulationResponse?.warnings?.length || simulationResponse?.metadata?.humanReadableError) {
67
+ if (revertOnSimulationError) {
68
+ const err = new Error('StakingMaticSimulationError')
69
+ err.message = simulationResponse?.metadata?.humanReadableError
70
+ err.reason = `warnings: ${simulationResponse.warnings.join(', ')}`
71
+ err.hint = 'delegate-matic-simulation-error'
72
+ throw err
73
+ } else {
74
+ console.warn(
75
+ 'Simulation for staking matic returned issues',
76
+ simulationResponse?.metadata?.humanReadableError
77
+ )
78
+ }
79
+ }
80
+
81
+ // For validation of the transactions, we need to check if the simulation produced the expected effects
82
+ // hasApprove, hasSend, hasReceive entries must be present
83
+ const { balanceChanges = Object.create(null) } = simulationResponse || Object.create(null)
84
+ const asArray = (v) => (Array.isArray(v) ? v : [])
85
+ const hasApprove = asArray(balanceChanges.willApprove).length > 0
86
+ const hasSend = asArray(balanceChanges.willSend).length > 0
87
+ const hasReceive = asArray(balanceChanges.willReceive).length > 0
88
+
89
+ if (!(hasApprove && hasSend && hasReceive)) {
90
+ if (revertOnSimulationError) {
91
+ const missing = []
92
+ if (!hasApprove) missing.push('willApprove')
93
+ if (!hasSend) missing.push('willSend')
94
+ if (!hasReceive) missing.push('willReceive')
95
+ const err = new Error('StakingMaticSimulationResultsError')
96
+ err.message = 'Simulation did not produce expected effects'
97
+ err.reason = `missing balanceChanges: ${missing.join(', ')}`
98
+ err.hint = 'delegate-matic-simulation-results-error'
99
+ throw err
100
+ } else {
101
+ console.warn(
102
+ 'Simulation for staking matic returned issues',
103
+ simulationResponse?.metadata?.humanReadableError
104
+ )
105
+ }
106
+ }
107
+ }
@@ -1,158 +1,299 @@
1
+ import { memoize } from '@exodus/basic-utils'
1
2
  import { isNumberUnit } from '@exodus/currency'
2
- import { estimateGasLimit, stakingProviderClientFactory } from '@exodus/ethereum-api'
3
- import BN from 'bn.js'
3
+ import {
4
+ estimateGasLimit,
5
+ getAggregateTransactionPricing,
6
+ getOptimisticTxLogEffects,
7
+ scaleGasLimitEstimate,
8
+ stakingProviderClientFactory,
9
+ } from '@exodus/ethereum-api'
4
10
 
5
- export function stakingServiceFactory({ assetClientInterface, server, stakingServer }) {
11
+ import { maticDelegateSimulateTransactions } from './matic-staking-utils.js'
12
+
13
+ export function stakingServiceFactory({ assetClientInterface, server: _server, stakingServer }) {
6
14
  const stakingProvider = stakingProviderClientFactory()
15
+ const assetName = 'ethereum'
7
16
 
8
17
  function amountToCurrency({ asset, amount }) {
9
18
  return isNumberUnit(amount) ? amount : asset.currency.parse(amount)
10
19
  }
11
20
 
12
- async function getStakeAssets() {
21
+ const getStakeAssets = memoize(async () => {
13
22
  const { polygon: asset, ethereum: feeAsset } = await assetClientInterface.getAssetsForNetwork({
14
- baseAssetName: 'ethereum', // Polygon token lives in ETH network
23
+ baseAssetName: assetName, // Polygon token lives in ETH network
15
24
  })
16
25
  return { asset, feeAsset }
26
+ })
27
+
28
+ // Helper function which selects the correct `asset` to use for when
29
+ // determining `feeData` for MATIC staking.
30
+ const resolveFeeData = async ({ feeData }) => {
31
+ const { feeAsset } = await getStakeAssets()
32
+ if (feeData) return feeData
33
+
34
+ console.warn(
35
+ 'The evm staking service was not explicitly passed `feeData`. This can result in transaction nondeterminism.'
36
+ )
37
+
38
+ return assetClientInterface.getFeeData({ assetName: feeAsset.name })
39
+ }
40
+
41
+ const getLatestFeeData = async () => {
42
+ const { feeAsset } = await getStakeAssets()
43
+ return assetClientInterface.getFeeData({ assetName: feeAsset.name })
44
+ }
45
+
46
+ const resolveOptionalFeeData = async ({ feeData }) => {
47
+ // If the caller provides truthy `feeData`, we can continue
48
+ // as normal.
49
+ if (feeData) return feeData
50
+
51
+ // If the caller specifically omits `feeData`, then we
52
+ // provide a backup without warning. This is useful for
53
+ // calls with no expectations on the caller to provide
54
+ // `feeData`, i.e. one-shot transactions, or transactions
55
+ // which do not render a fee estimation.
56
+ return getLatestFeeData()
17
57
  }
18
58
 
19
- async function approveDelegateAmount({ walletAccount, amount } = {}) {
20
- const { asset } = await getStakeAssets()
59
+ const getDelegatorAddress = async ({ walletAccount }) => {
21
60
  const address = await assetClientInterface.getReceiveAddress({
22
- assetName: asset.name,
61
+ assetName,
23
62
  walletAccount,
24
63
  })
25
- const delegatorAddress = address.toLowerCase()
64
+ return address.toLowerCase()
65
+ }
66
+
67
+ async function approveDelegateAmount({ walletAccount, amount, feeData } = {}) {
68
+ feeData = await resolveOptionalFeeData({ feeData })
69
+
70
+ const [delegatorAddress, { asset }] = await Promise.all([
71
+ getDelegatorAddress({ walletAccount }),
72
+ getStakeAssets(),
73
+ ])
26
74
 
27
75
  amount = amountToCurrency({ asset, amount })
28
76
 
29
77
  const txApproveData = await stakingServer.approveStakeManager(amount)
30
- const { gasPrice, gasLimit, fee } = await estimateTxFee(
31
- delegatorAddress,
32
- asset.contracts.TOKEN_CONTRACT,
33
- txApproveData
34
- )
78
+ const { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee({
79
+ from: delegatorAddress,
80
+ to: stakingServer.polygonContract.address,
81
+ txInput: txApproveData,
82
+ feeData,
83
+ })
35
84
 
36
85
  return prepareAndSendTx({
37
86
  walletAccount,
38
87
  waitForConfirmation: true,
39
- to: asset.contracts.TOKEN_CONTRACT,
88
+ to: stakingServer.polygonContract.address,
40
89
  txData: txApproveData,
41
90
  gasPrice,
42
91
  gasLimit,
43
- fee,
92
+ tipGasPrice,
93
+ feeData,
44
94
  })
45
95
  }
46
96
 
47
- async function delegate({ walletAccount, amount } = {}) {
48
- const { asset } = await getStakeAssets()
49
- const address = await assetClientInterface.getReceiveAddress({
50
- assetName: asset.name,
97
+ /**
98
+ * Delegate MATIC tokens for staking.
99
+ * @param {walletAccount} params.walletAccount - The walletAccount for the delegator (required).
100
+ * @param {NumberUnit} params.amount - The amount of MATIC to delegate (required).
101
+ * @param {FeeData} params.feeData - Optional feeData to use for the transaction (optional, if not provided will be obtained from the asset client interface).
102
+ * @param {Boolean} params.revertOnSimulationError - Optional boolean to revert on simulation error (optional, default is true). Allows us to quickly react to simulator issues.
103
+ */
104
+ async function delegate({ walletAccount, amount, feeData, revertOnSimulationError = true } = {}) {
105
+ const [delegatorAddress, { asset, feeAsset }, resolvedFeeData] = await Promise.all([
106
+ getDelegatorAddress({ walletAccount }),
107
+ getStakeAssets(),
108
+ resolveFeeData({ feeData }),
109
+ ])
110
+
111
+ feeData = resolvedFeeData
112
+
113
+ amount = amountToCurrency({ asset, amount })
114
+
115
+ const baseNonce = await feeAsset.getNonce({
116
+ asset: feeAsset,
117
+ fromAddress: delegatorAddress.toString(),
51
118
  walletAccount,
52
119
  })
53
- const delegatorAddress = address.toLowerCase()
54
120
 
55
- amount = amountToCurrency({ asset, amount })
121
+ const [txApproveData, txDelegateData] = await Promise.all([
122
+ stakingServer.approveStakeManager(amount),
123
+ stakingServer.delegate({ amount }),
124
+ ])
56
125
 
57
- // const txApproveData = await stakingServer.approveStakeManager(amount)
58
- // let { gasPrice, gasLimit, fee } = await estimateTxFee(
59
- // delegatorAddress,
60
- // contracts.TOKEN_CONTRACT,
61
- // txApproveData
62
- // )
63
- // await prepareAndSendTx({
64
- // walletAccount,
65
- // waitForConfirmation: true,
66
- // to: contracts.TOKEN_CONTRACT,
67
- // txData: txApproveData,
68
- // gasPrice,
69
- // gasLimit,
70
- // fee,
71
- // })
72
-
73
- const txDelegateData = await stakingServer.delegate({ amount })
74
- const { gasPrice, gasLimit, fee } = await estimateTxFee(
75
- delegatorAddress,
76
- asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
77
- txDelegateData
78
- )
126
+ const {
127
+ gasPrice,
128
+ gasLimit: approveGasLimit,
129
+ tipGasPrice,
130
+ } = await estimateTxFee({
131
+ from: delegatorAddress,
132
+ to: stakingServer.polygonContract.address,
133
+ txInput: txApproveData,
134
+ feeData,
135
+ })
79
136
 
80
- const txId = await prepareAndSendTx({
137
+ // Estimate based on the average gas usage for the delegate transaction. 250000 is the average.
138
+ const { gasLimit: estimatedDelegateGasLimit } = await estimateDelegateTxFee({
139
+ feeData,
140
+ })
141
+
142
+ const createTxBaseArgs = {
143
+ asset: feeAsset,
81
144
  walletAccount,
82
- to: asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
83
- txData: txDelegateData,
145
+ fromAddress: delegatorAddress.toString(),
146
+ amount: feeAsset.currency.ZERO,
147
+ tipGasPrice,
84
148
  gasPrice,
85
- gasLimit,
86
- fee,
149
+ }
150
+ const [unsignedTxApprove, unsignedTxDelegate] = await Promise.all([
151
+ feeAsset.api.createTx({
152
+ ...createTxBaseArgs,
153
+ toAddress: stakingServer.polygonContract.address,
154
+ txInput: txApproveData,
155
+ gasLimit: approveGasLimit,
156
+ nonce: baseNonce,
157
+ }),
158
+ feeAsset.api.createTx({
159
+ ...createTxBaseArgs,
160
+ toAddress: stakingServer.validatorShareContract.address,
161
+ txInput: txDelegateData,
162
+ gasLimit: estimatedDelegateGasLimit,
163
+ nonce: baseNonce + 1,
164
+ }),
165
+ ])
166
+
167
+ await maticDelegateSimulateTransactions({
168
+ asset,
169
+ unsignedTxApprove,
170
+ unsignedTxDelegate,
171
+ senderAddress: delegatorAddress.toString(),
172
+ revertOnSimulationError,
87
173
  })
88
174
 
175
+ // Sign transactions
176
+ const [approveSigned, delegateSigned] = await Promise.all([
177
+ assetClientInterface.signTransaction({
178
+ assetName: feeAsset.name,
179
+ unsignedTx: unsignedTxApprove.unsignedTx,
180
+ walletAccount,
181
+ }),
182
+ assetClientInterface.signTransaction({
183
+ assetName: feeAsset.name,
184
+ unsignedTx: unsignedTxDelegate.unsignedTx,
185
+ walletAccount,
186
+ }),
187
+ ])
188
+
189
+ // Pre-compute txIDs
190
+ const approveTxId = `0x${approveSigned.txId.toString('hex')}`
191
+ const delegateTxId = `0x${delegateSigned.txId.toString('hex')}`
192
+
193
+ const bundleResponse = await feeAsset.broadcastPrivateBundle({
194
+ txs: [approveSigned, delegateSigned].map(({ rawTx }) => rawTx),
195
+ })
196
+ const bundleHash = bundleResponse?.bundleHash
197
+
198
+ const { optimisticTxLogEffects: approveOptimisticTxLogEffects } =
199
+ await getOptimisticTxLogEffects({
200
+ asset: feeAsset,
201
+ assetClientInterface,
202
+ fromAddress: delegatorAddress.toString(),
203
+ txId: approveTxId,
204
+ unsignedTx: unsignedTxApprove.unsignedTx,
205
+ walletAccount,
206
+ bundleId: bundleHash ?? undefined,
207
+ })
208
+
209
+ const { optimisticTxLogEffects: delegateOptimisticTxLogEffects } =
210
+ await getOptimisticTxLogEffects({
211
+ asset: feeAsset,
212
+ assetClientInterface,
213
+ fromAddress: delegatorAddress.toString(),
214
+ txId: delegateTxId,
215
+ unsignedTx: unsignedTxDelegate.unsignedTx,
216
+ walletAccount,
217
+ bundleId: bundleHash ?? undefined,
218
+ })
219
+ // Combine optimistic effects from both transactions
220
+ const tokenOptimisticEffects = [
221
+ ...approveOptimisticTxLogEffects,
222
+ ...delegateOptimisticTxLogEffects,
223
+ ]
224
+
225
+ for (const optimisticEffect of tokenOptimisticEffects) {
226
+ await assetClientInterface.updateTxLogAndNotify(optimisticEffect)
227
+ }
228
+
89
229
  await stakingProvider.notifyStaking({
90
- txId,
230
+ txId: delegateTxId,
91
231
  asset: asset.name,
92
232
  delegator: delegatorAddress,
93
233
  amount: amount.toBaseString(),
94
234
  })
95
-
96
- return txId
235
+ return delegateTxId
97
236
  }
98
237
 
99
- async function undelegate({ walletAccount, amount } = {}) {
100
- const { asset } = await getStakeAssets()
101
- const address = await assetClientInterface.getReceiveAddress({
102
- assetName: asset.name,
103
- walletAccount,
104
- })
105
- const delegatorAddress = address.toLowerCase()
238
+ async function undelegate({ walletAccount, amount, feeData, waitForConfirmation = false } = {}) {
239
+ feeData = await resolveOptionalFeeData({ feeData })
240
+
241
+ const [delegatorAddress, { asset }] = await Promise.all([
242
+ getDelegatorAddress({ walletAccount }),
243
+ getStakeAssets(),
244
+ ])
106
245
 
107
246
  amount = amountToCurrency({ asset, amount })
108
247
 
109
248
  const txUndelegateData = await stakingServer.undelegate({ amount })
110
- const { gasPrice, gasLimit, fee } = await estimateTxFee(
111
- delegatorAddress.toLowerCase(),
112
- asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
113
- txUndelegateData
114
- )
249
+ const { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee({
250
+ from: delegatorAddress.toLowerCase(),
251
+ to: stakingServer.validatorShareContract.address,
252
+ txInput: txUndelegateData,
253
+ feeData,
254
+ })
115
255
  return prepareAndSendTx({
116
256
  walletAccount,
117
- to: asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
257
+ to: stakingServer.validatorShareContract.address,
118
258
  txData: txUndelegateData,
119
259
  gasPrice,
120
260
  gasLimit,
121
- fee,
261
+ tipGasPrice,
262
+ waitForConfirmation,
263
+ feeData,
122
264
  })
123
265
  }
124
266
 
125
- async function claimRewards({ walletAccount } = {}) {
126
- const { asset } = await getStakeAssets()
127
- const address = await assetClientInterface.getReceiveAddress({
128
- assetName: asset.name,
129
- walletAccount,
130
- })
131
- const delegatorAddress = address.toLowerCase()
267
+ async function claimRewards({ walletAccount, feeData } = {}) {
268
+ feeData = await resolveOptionalFeeData({ feeData })
269
+
270
+ const delegatorAddress = await getDelegatorAddress({ walletAccount })
132
271
 
133
272
  const txWithdrawRewardsData = await stakingServer.withdrawRewards()
134
- const { gasPrice, gasLimit, fee } = await estimateTxFee(
135
- delegatorAddress,
136
- asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
137
- txWithdrawRewardsData
138
- )
273
+ const { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee({
274
+ from: delegatorAddress,
275
+ to: stakingServer.validatorShareContract.address,
276
+ txInput: txWithdrawRewardsData,
277
+ feeData,
278
+ })
139
279
  return prepareAndSendTx({
140
280
  walletAccount,
141
- to: asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
281
+ to: stakingServer.validatorShareContract.address,
142
282
  txData: txWithdrawRewardsData,
143
283
  gasPrice,
144
284
  gasLimit,
145
- fee,
285
+ tipGasPrice,
286
+ feeData,
146
287
  })
147
288
  }
148
289
 
149
- async function claimUndelegatedBalance({ walletAccount, unbondNonce } = {}) {
150
- const { asset } = await getStakeAssets()
151
- const address = await assetClientInterface.getReceiveAddress({
152
- assetName: asset.name,
153
- walletAccount,
154
- })
155
- const delegatorAddress = address.toLowerCase()
290
+ async function claimUndelegatedBalance({ walletAccount, unbondNonce, feeData } = {}) {
291
+ feeData = await resolveOptionalFeeData({ feeData })
292
+
293
+ const [delegatorAddress, { asset }] = await Promise.all([
294
+ getDelegatorAddress({ walletAccount }),
295
+ getStakeAssets(),
296
+ ])
156
297
 
157
298
  const { currency } = asset
158
299
  const unstakedClaimInfo = await fetchUnstakedClaimInfo({
@@ -168,18 +309,20 @@ export function stakingServiceFactory({ assetClientInterface, server, stakingSer
168
309
  })
169
310
 
170
311
  const txClaimUndelegatedData = await stakingServer.claimUndelegatedBalance({ unbondNonce })
171
- const { gasPrice, gasLimit, fee } = await estimateTxFee(
172
- delegatorAddress,
173
- asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
174
- txClaimUndelegatedData
175
- )
312
+ const { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee({
313
+ from: delegatorAddress,
314
+ to: stakingServer.validatorShareContract.address,
315
+ txInput: txClaimUndelegatedData,
316
+ feeData,
317
+ })
176
318
  const txId = await prepareAndSendTx({
177
319
  walletAccount,
178
- to: asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
320
+ to: stakingServer.validatorShareContract.address,
179
321
  txData: txClaimUndelegatedData,
180
322
  gasPrice,
181
323
  gasLimit,
182
- fee,
324
+ tipGasPrice,
325
+ feeData,
183
326
  })
184
327
 
185
328
  await stakingProvider.notifyUnstaking({
@@ -192,19 +335,29 @@ export function stakingServiceFactory({ assetClientInterface, server, stakingSer
192
335
  return txId
193
336
  }
194
337
 
195
- async function estimateDelegateOperation({ walletAccount, operation, args }) {
196
- const delegateOperation = stakingServer[operation]
338
+ async function estimateDelegateOperation({
339
+ walletAccount,
340
+ operation,
341
+ args,
342
+ // NOTE: When estimating transactions, ideally we'd expect the `feeData`
343
+ // that we intend to send the transaction using. If this is not
344
+ // defined, we'll fallback to a default (with a warning).
345
+ feeData,
346
+ }) {
347
+ // HACK: For delegation transactions, we must fall back to the
348
+ // custom implementation, since we can't currently estimate
349
+ // the transaction due to a dependence upon approvals.
350
+ if (operation === 'delegate') return estimateDelegateTxFee({ feeData })
351
+
352
+ const [delegatorAddress, { asset }] = await Promise.all([
353
+ getDelegatorAddress({ walletAccount }),
354
+ getStakeAssets(),
355
+ ])
197
356
 
198
- if (!delegateOperation) {
199
- return
200
- }
357
+ feeData = await resolveFeeData({ feeData })
201
358
 
202
- const { asset } = await getStakeAssets()
203
- const address = await assetClientInterface.getReceiveAddress({
204
- assetName: asset.name,
205
- walletAccount,
206
- })
207
- const delegatorAddress = address.toLowerCase()
359
+ const delegateOperation = stakingServer[operation]
360
+ if (!delegateOperation) return
208
361
 
209
362
  const { amount } = args
210
363
  if (amount) {
@@ -212,11 +365,12 @@ export function stakingServiceFactory({ assetClientInterface, server, stakingSer
212
365
  }
213
366
 
214
367
  const operationTxData = await delegateOperation({ ...args, walletAccount })
215
- const { fee } = await estimateTxFee(
216
- delegatorAddress,
217
- asset.contracts.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
218
- operationTxData
219
- )
368
+ const { fee } = await estimateTxFee({
369
+ from: delegatorAddress,
370
+ to: stakingServer.validatorShareContract.address,
371
+ txInput: operationTxData,
372
+ feeData,
373
+ })
220
374
 
221
375
  return fee
222
376
  }
@@ -231,21 +385,25 @@ export function stakingServiceFactory({ assetClientInterface, server, stakingSer
231
385
  * This is just for displaying purposes and it's just an aproximation of the delegate gas cost,
232
386
  * NOT the real fee cost
233
387
  */
234
- async function estimateDelegateTxFee() {
388
+ async function estimateDelegateTxFee({ feeData } = Object.create(null)) {
235
389
  // approx gas limits
236
390
  const { feeAsset } = await getStakeAssets()
237
- const erc20ApproveGas = 4900
238
- const delegateGas = 240_000
239
- const gasPrice = parseInt(await server.gasPrice(), 16)
240
- const extraPercentage = 20
241
-
242
- const gasLimit = erc20ApproveGas + delegateGas
243
- const gasLimitWithBuffer = new BN(gasLimit)
244
- .imuln(100 + extraPercentage)
245
- .idivn(100)
246
- .toString()
247
391
 
248
- const fee = new BN(gasLimitWithBuffer).mul(new BN(gasPrice))
392
+ feeData = await resolveFeeData({ feeData })
393
+
394
+ // TODO: update estimation to use a mock source address for
395
+ // deposits so we can simulate the necessary approvals,
396
+ // we shouldn't maintain constants like these
397
+ const erc20ApproveGas = 80_000
398
+ const delegateGas = 250_000
399
+
400
+ const { gasPrice } = getAggregateTransactionPricing({ baseAsset: feeAsset, feeData })
401
+
402
+ const gasLimitWithBuffer = scaleGasLimitEstimate({
403
+ estimatedGasLimit: BigInt(erc20ApproveGas + delegateGas),
404
+ })
405
+
406
+ const fee = BigInt(gasLimitWithBuffer) * BigInt(gasPrice.toBaseNumber())
249
407
 
250
408
  return {
251
409
  gasLimit: gasLimitWithBuffer,
@@ -254,31 +412,22 @@ export function stakingServiceFactory({ assetClientInterface, server, stakingSer
254
412
  }
255
413
  }
256
414
 
257
- async function estimateTxFee(from, to, txInput, gasPrice = '0x0') {
415
+ async function estimateTxFee({ from, to, txInput, feeData }) {
258
416
  const { feeAsset } = await getStakeAssets()
259
417
 
260
418
  const amount = feeAsset.currency.ZERO
419
+
261
420
  const gasLimit = await estimateGasLimit({
262
421
  asset: feeAsset,
263
422
  fromAddress: from,
264
423
  toAddress: to,
265
- amount,
424
+ amount, // staking contracts does not require ETH amount to interact with
266
425
  data: txInput,
267
- gasPrice,
426
+ gasPrice: '0x0', // DISABLE_BALANCE_CHECKS equivalent
268
427
  })
269
428
 
270
- if (gasPrice === '0x0') {
271
- gasPrice = await server.gasPrice()
272
- }
273
-
274
- gasPrice = parseInt(gasPrice, 16)
275
- const fee = new BN(gasPrice).mul(new BN(gasLimit))
276
-
277
- return {
278
- gasLimit,
279
- gasPrice: feeAsset.currency.baseUnit(gasPrice),
280
- fee: feeAsset.currency.baseUnit(fee.toString()),
281
- }
429
+ const fee = feeAsset.api.getFee({ asset: feeAsset, feeData, gasLimit, amount })
430
+ return { ...fee, gasLimit }
282
431
  }
283
432
 
284
433
  async function prepareAndSendTx({
@@ -287,30 +436,25 @@ export function stakingServiceFactory({ assetClientInterface, server, stakingSer
287
436
  txData: txInput,
288
437
  gasPrice,
289
438
  gasLimit,
290
- fee,
439
+ tipGasPrice,
291
440
  waitForConfirmation = false,
441
+ feeData,
292
442
  } = {}) {
293
- const { asset, feeAsset } = await getStakeAssets()
294
-
443
+ const { feeAsset } = await getStakeAssets()
295
444
  const sendTxArgs = {
296
445
  asset: feeAsset,
297
446
  walletAccount,
298
447
  address: to,
299
448
  amount: feeAsset.currency.ZERO,
300
- // used in desktop,
301
- // remove once, txSend is unified
302
- receiver: {
303
- address: to,
304
- amount: feeAsset.currency.ZERO,
305
- },
306
449
  txInput,
307
450
  gasPrice,
308
451
  gasLimit,
309
- feeAmount: fee,
452
+ tipGasPrice,
310
453
  waitForConfirmation,
454
+ feeData,
311
455
  }
312
456
 
313
- const { txId } = await asset.api.sendTx(sendTxArgs)
457
+ const { txId } = await feeAsset.api.sendTx(sendTxArgs)
314
458
 
315
459
  return txId
316
460
  }
@@ -318,12 +462,18 @@ export function stakingServiceFactory({ assetClientInterface, server, stakingSer
318
462
  const getStakingInfo = getPolygonStakingInfo({ assetClientInterface, stakingServer })
319
463
 
320
464
  return {
465
+ // Unified API (used by stakingApiFactory when exposed on asset.api.staking)
466
+ approveStake: approveDelegateAmount,
467
+ stake: delegate,
468
+ unstake: undelegate,
469
+ claimUnstaked: claimUndelegatedBalance,
470
+ claimRewards,
471
+ getStakingInfo,
472
+ // Legacy names (for direct service usage)
321
473
  approveDelegateAmount,
322
474
  delegate,
323
475
  undelegate,
324
- claimRewards,
325
476
  claimUndelegatedBalance,
326
- getStakingInfo,
327
477
  estimateDelegateTxFee,
328
478
  estimateDelegateOperation,
329
479
  }
@@ -406,18 +556,20 @@ async function getUnstakedUnclaimedInfo({
406
556
  }
407
557
 
408
558
  async function fetchRewardsInfo({ stakingServer, delegator, currency }) {
409
- const [minRewardsToWithdraw, rewardsBalance] = await Promise.all([
559
+ const [minRewardsToWithdraw, lastRewards, rewardsBalance] = await Promise.all([
410
560
  stakingServer.getMinRewardsToWithdraw(),
411
561
  stakingServer.getLiquidRewards(delegator),
562
+ stakingServer.getTotalRewards(delegator),
412
563
  ])
413
- const withdrawable = rewardsBalance.sub(minRewardsToWithdraw).gte(currency.ZERO)
414
- ? rewardsBalance
564
+ const withdrawable = lastRewards.sub(minRewardsToWithdraw).gte(currency.ZERO)
565
+ ? lastRewards
415
566
  : currency.ZERO
416
567
 
417
568
  return {
418
- rewardsBalance,
569
+ rewardsBalance: rewardsBalance.add(lastRewards), // all time accrued rewards
570
+ liquidRewards: lastRewards, // current pending rewards (on-chain)
419
571
  minRewardsToWithdraw,
420
- withdrawable,
572
+ withdrawable, // unclaimed rewards
421
573
  }
422
574
  }
423
575
 
@@ -430,7 +582,7 @@ export function getPolygonStakingInfo({ assetClientInterface, stakingServer }) {
430
582
  const delegator = address.toLowerCase()
431
583
  const [
432
584
  delegatedBalance,
433
- { rewardsBalance, minRewardsToWithdraw, withdrawable },
585
+ { rewardsBalance, liquidRewards, minRewardsToWithdraw, withdrawable },
434
586
  { unbondNonce, withdrawalDelay, currentEpoch, withdrawExchangeRate },
435
587
  ] = await Promise.all([
436
588
  stakingServer.getTotalStake(delegator),
@@ -452,6 +604,7 @@ export function getPolygonStakingInfo({ assetClientInterface, stakingServer }) {
452
604
 
453
605
  return {
454
606
  rewardsBalance,
607
+ liquidRewards,
455
608
  withdrawable,
456
609
  unbondNonce,
457
610
  isDelegating,
package/src/staking.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { ethereumStakingDeps } from '@exodus/ethereum-api'
1
2
  import { ethStakeAccountState } from '@exodus/ethereum-lib'
2
3
  import { asset } from '@exodus/ethereum-meta'
3
4
 
@@ -9,5 +10,6 @@ export const stakingConfiguration = {
9
10
  }
10
11
 
11
12
  export const stakingDependencies = {
13
+ ethereum: ethereumStakingDeps,
12
14
  polygon: polygonStakingDeps,
13
15
  }