@exodus/ethereum-api 8.64.2 → 8.64.4

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,24 @@
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.64.4](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.64.3...@exodus/ethereum-api@8.64.4) (2026-02-09)
7
+
8
+ **Note:** Version bump only for package @exodus/ethereum-api
9
+
10
+
11
+
12
+
13
+
14
+ ## [8.64.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.64.2...@exodus/ethereum-api@8.64.3) (2026-01-26)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+
20
+ * fix: drop third party tx input from clarity tx log items (#7317)
21
+
22
+
23
+
6
24
  ## [8.64.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.64.1...@exodus/ethereum-api@8.64.2) (2026-01-23)
7
25
 
8
26
  **Note:** Version bump only for package @exodus/ethereum-api
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.64.2",
3
+ "version": "8.64.4",
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",
@@ -67,5 +67,5 @@
67
67
  "type": "git",
68
68
  "url": "git+https://github.com/ExodusMovement/assets.git"
69
69
  },
70
- "gitHead": "d6aa274a62e489f5dd27e11088c56b3a64903779"
70
+ "gitHead": "ebcc3fe022dfc1e21e3ccdc00faf0769e8fb3092"
71
71
  }
@@ -37,7 +37,7 @@ import { getBalancesFactory } from './get-balances.js'
37
37
  import { getFeeFactory } from './get-fee.js'
38
38
  import { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas/index.js'
39
39
  import { serverBasedFeeMonitorFactoryFactory } from './server-based-fee-monitor.js'
40
- import { createStakingApi } from './staking-api.js'
40
+ import { stakingApiFactory } from './staking/api/index.js'
41
41
  import { createTxFactory } from './tx-create.js'
42
42
  import { txSendFactory } from './tx-send/index.js'
43
43
  import { createWeb3API } from './web3/index.js'
@@ -63,7 +63,8 @@ export const createAssetFactory = ({
63
63
  monitorType: defaultMonitorType = 'magnifier',
64
64
  nfts: defaultNfts = false,
65
65
  serverUrl: defaultServerUrl,
66
- stakingConfiguration = {},
66
+ stakingConfiguration = Object.create(null),
67
+ stakingDependencies = Object.create(null),
67
68
  useEip1191ChainIdChecksum = false,
68
69
  forceGasLimitEstimation = false,
69
70
  rpcBalanceAssetNames = [],
@@ -156,7 +157,16 @@ export const createAssetFactory = ({
156
157
  })
157
158
 
158
159
  const { createToken, getTokens } = createTokenFactory(
159
- { address, bip44, keys, getBalances },
160
+ {
161
+ address,
162
+ bip44,
163
+ keys,
164
+ getBalances,
165
+ assetClientInterface,
166
+ server,
167
+ stakingConfiguration,
168
+ stakingDependencies,
169
+ },
160
170
  assets
161
171
  )
162
172
 
@@ -289,7 +299,17 @@ export const createAssetFactory = ({
289
299
  signHardware: signHardwareFactory({ baseAssetName: asset.name }),
290
300
  signMessage: ({ message, privateKey, signer }) =>
291
301
  signer ? signMessageWithSigner({ message, signer }) : signMessage({ privateKey, message }),
292
- ...(supportsStaking && { staking: createStakingApi({ network: asset.name }) }),
302
+ ...(supportsStaking &&
303
+ stakingDependencies[asset.name] && {
304
+ staking: stakingApiFactory({
305
+ assetName: asset.name,
306
+ currency: asset.currency,
307
+ assetClientInterface,
308
+ server,
309
+ stakingConfiguration: stakingConfiguration[asset.name],
310
+ stakingDependencies: stakingDependencies[asset.name],
311
+ }),
312
+ }),
293
313
  validateAssetId: address.validate,
294
314
  web3: createWeb3API({ asset }),
295
315
  }
@@ -2,12 +2,12 @@ import { ASSET_FAMILY } from '@exodus/assets'
2
2
  import { createContract } from '@exodus/ethereum-lib'
3
3
  import assert from 'minimalistic-assert'
4
4
 
5
+ import { stakingApiFactory } from './staking/api/index.js'
5
6
  import {
6
7
  isPolygonClaimUndelegate,
7
8
  isPolygonDelegate,
8
9
  isPolygonUndelegate,
9
10
  } from './staking/matic/index.js'
10
- import { createStakingApi } from './staking-api.js'
11
11
 
12
12
  const defaultTokenFeatures = {
13
13
  family: ASSET_FAMILY.EVM,
@@ -35,31 +35,50 @@ const getPolygonActivityTxs = ({ txs }) =>
35
35
  const getActivityTxs = ({ txs }) => txs.filter((tx) => !smallbalanceTx(tx))
36
36
 
37
37
  const getCreateBaseToken =
38
- ({ getBalances, ...props }) =>
38
+ ({
39
+ getBalances,
40
+ assetClientInterface,
41
+ server,
42
+ stakingConfiguration = Object.create(null),
43
+ stakingDependencies = Object.create(null),
44
+ ...props
45
+ }) =>
39
46
  ({ name, contract, features, ...tokenDef }) => {
40
47
  assert(getBalances, 'getBalances is required')
41
48
  const tokenSpecificFeatures = name === 'polygon' ? { staking: {} } : {}
42
- const tokenSpecificApiFunctions =
43
- name === 'polygon' ? { staking: createStakingApi({ network: name }) } : {}
44
49
 
45
50
  const tokenFeatures = { ...defaultTokenFeatures, ...features, ...tokenSpecificFeatures }
46
- return {
51
+ const baseApi = {
52
+ getActivityTxs: name === 'polygon' ? getPolygonActivityTxs : getActivityTxs,
53
+ features: tokenFeatures,
54
+ hasFeature: (feature) => !!tokenFeatures[feature], // @deprecated use api.features instead
55
+ getBalances,
56
+ getTxLogFilter: name === 'polygon' ? getPolygonTxLogFilter : getTxLogFilter,
57
+ }
58
+
59
+ const tokenAsset = {
47
60
  ...tokenDef,
48
61
  addresses: contract,
49
62
  assetId: contract.current.toLowerCase(),
50
63
  contract: createContract(contract.current, name),
51
64
  gasLimit: 120_000,
52
65
  name,
53
- api: {
54
- getActivityTxs: name === 'polygon' ? getPolygonActivityTxs : getActivityTxs,
55
- features: tokenFeatures,
56
- hasFeature: (feature) => !!tokenFeatures[feature], // @deprecated use api.features instead
57
- getBalances,
58
- getTxLogFilter: name === 'polygon' ? getPolygonTxLogFilter : getTxLogFilter,
59
- ...tokenSpecificApiFunctions,
60
- },
61
- ...props, // override props above, add new props
66
+ api: baseApi,
67
+ ...props,
62
68
  }
69
+
70
+ if (name === 'polygon' && stakingDependencies[name]) {
71
+ baseApi.staking = stakingApiFactory({
72
+ assetName: tokenAsset.name,
73
+ currency: tokenAsset.currency,
74
+ assetClientInterface,
75
+ server,
76
+ stakingConfiguration: stakingConfiguration[name],
77
+ stakingDependencies: stakingDependencies[name],
78
+ })
79
+ }
80
+
81
+ return tokenAsset
63
82
  }
64
83
 
65
84
  export const createTokenFactory = (props, assets) => {
@@ -14,7 +14,7 @@ import { getLatestCanonicalAbsoluteBalanceTx } from './tx-log/clarity-utils/inde
14
14
  * Missing fields that should be added to getBalancesFactory return value:
15
15
  *
16
16
  * 1. `rewards` / `liquidRewards` - Currently, accountState.staking[asset.name] contains
17
- * rewardsBalance and liquidRewards (from getPolygonStakingInfo / getEthereumStakingInfo),
17
+ * rewardsBalance and liquidRewards (from asset.api.staking.getStakingInfo accountState.staking),
18
18
  * but getBalancesFactory doesn't read or expose them. Need a getRewards() helper similar
19
19
  * to getStaked() and getStaking().
20
20
  *
@@ -1,6 +1,5 @@
1
1
  import assert from 'minimalistic-assert'
2
2
 
3
- import { getEthereumStakingInfo, getPolygonStakingInfo } from '../staking/index.js'
4
3
  import processTxLog from '../tx-log-staking-processor/index.js'
5
4
 
6
5
  export const createEthereumHooks = ({
@@ -32,17 +31,13 @@ export const createEthereumHooks = ({
32
31
  }
33
32
 
34
33
  for (const asset of stakingAssets) {
35
- const stakingAssetName = asset.name
36
- const getStakingInfo =
37
- stakingAssetName === 'polygon' ? getPolygonStakingInfo : getEthereumStakingInfo
34
+ const stakingApi = asset.api?.staking
35
+ if (!stakingApi?.getStakingInfo) continue
38
36
 
39
- // asset may not be enabled in the wallet
40
- const assetStakingInfo = await getStakingInfo({
37
+ const assetStakingInfo = await stakingApi.getStakingInfo({
41
38
  address: userAddress.toString(),
42
- asset,
43
- server,
44
39
  })
45
- stakingInfo.staking[stakingAssetName] = assetStakingInfo
40
+ stakingInfo.staking[asset.name] = assetStakingInfo
46
41
  }
47
42
 
48
43
  const batch = assetClientInterface.createOperationsBatch()
package/src/index.js CHANGED
@@ -26,17 +26,24 @@ export {
26
26
  DEFAULT_GAS_LIMIT_MULTIPLIER,
27
27
  estimateGasLimit,
28
28
  fetchGasLimit,
29
+ scaleGasLimitEstimate,
29
30
  } from './gas-estimation.js'
30
31
 
31
32
  export { createEvmServer, getServer } from './exodus-eth-server/index.js'
32
33
 
33
- export { EthereumMonitor, EthereumNoHistoryMonitor, ClarityMonitor } from './tx-log/index.js'
34
+ export {
35
+ EthereumMonitor,
36
+ EthereumNoHistoryMonitor,
37
+ ClarityMonitor,
38
+ getOptimisticTxLogEffects,
39
+ } from './tx-log/index.js'
34
40
 
35
41
  export { getStakingHistoryBalance, getBalancesFactory } from './get-balances.js'
36
42
 
37
43
  export {
38
44
  FantomStaking,
39
45
  stakingProviderClientFactory,
46
+ ethereumStakingDeps,
40
47
  getEthereumStakingInfo,
41
48
  createEthereumStakingService,
42
49
  EthereumStaking,
@@ -46,8 +53,6 @@ export {
46
53
  isEthereumUndelegate,
47
54
  isEthereumClaimUndelegate,
48
55
  MaticStakingApi,
49
- createPolygonStakingService,
50
- getPolygonStakingInfo,
51
56
  isPolygonTx,
52
57
  isPolygonDelegate,
53
58
  isPolygonUndelegate,
@@ -71,6 +76,8 @@ export {
71
76
 
72
77
  export { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas/index.js'
73
78
 
79
+ export { getAggregateTransactionPricing } from './get-fee.js'
80
+
74
81
  export {
75
82
  fromHexToString,
76
83
  fromHexToBigInt,
@@ -1,14 +1,15 @@
1
- import { pick } from '@exodus/basic-utils'
2
1
  import assert from 'minimalistic-assert'
3
2
 
4
3
  export const stakingApiFactory = ({
5
- asset,
4
+ assetName,
5
+ currency,
6
6
  assetClientInterface,
7
7
  server, // coin node server
8
8
  stakingConfiguration,
9
9
  stakingDependencies,
10
10
  }) => {
11
- assert(asset, '"asset" is required')
11
+ assert(assetName, '"assetName" is required')
12
+ assert(currency, '"currency" is required')
12
13
  assert(assetClientInterface, '"assetClientInterface" is required')
13
14
  assert(server, '"server" is required')
14
15
  assert(stakingConfiguration, '"stakingConfiguration" is required')
@@ -17,7 +18,7 @@ export const stakingApiFactory = ({
17
18
  const { contracts, minAmount } = stakingConfiguration
18
19
  const { stakingServerFactory, stakingServiceFactory } = stakingDependencies
19
20
 
20
- const stakingServer = stakingServerFactory({ asset, server, minAmount, contracts })
21
+ const stakingServer = stakingServerFactory({ assetName, currency, server, minAmount, contracts })
21
22
 
22
23
  const stakingService = stakingServiceFactory({
23
24
  assetClientInterface,
@@ -26,9 +27,9 @@ export const stakingApiFactory = ({
26
27
  })
27
28
 
28
29
  return {
30
+ ...stakingService,
29
31
  isStaking: async ({ isDelegating }) => isDelegating,
30
32
  isUnstaking: async ({ isUndelegateInProgress }) => isUndelegateInProgress,
31
33
  isUnstaked: async ({ canClaimUndelegateBalance }) => canClaimUndelegateBalance,
32
- ...pick(stakingService, ['approveStake', 'stake', 'unstake', 'claimUnstaked', 'claimRewards']),
33
34
  }
34
35
  }
@@ -42,20 +42,21 @@ export class EthereumStaking {
42
42
  }
43
43
 
44
44
  constructor(
45
- asset, // ethereum or ethereumholesky for testnet
45
+ assetName, // ethereum or ethereumholesky for testnet
46
+ currency,
46
47
  minAmount = MIN_AMOUNT,
47
48
  server
48
49
  ) {
49
- this.asset = asset
50
+ this.asset = { name: assetName, currency }
50
51
  const accountingAddress =
51
- EthereumStaking.addresses[asset.name].EVERSTAKE_ADDRESS_CONTRACT_ACCOUNTING
52
- const poolAddress = EthereumStaking.addresses[asset.name].EVERSTAKE_ADDRESS_CONTRACT_POOL
52
+ EthereumStaking.addresses[assetName].EVERSTAKE_ADDRESS_CONTRACT_ACCOUNTING
53
+ const poolAddress = EthereumStaking.addresses[assetName].EVERSTAKE_ADDRESS_CONTRACT_POOL
53
54
 
54
55
  this.contractAccounting = createContract(accountingAddress, 'ethStakingAccounting')
55
56
  this.contractPool = createContract(poolAddress, 'ethStakingPool')
56
57
  this.accountingAddress = accountingAddress
57
58
  this.poolAddress = poolAddress
58
- this.minAmount = asset.currency.defaultUnit(minAmount)
59
+ this.minAmount = currency.defaultUnit(minAmount)
59
60
  this.server = server
60
61
  }
61
62
 
@@ -0,0 +1,33 @@
1
+ import { EthereumStaking } from './api.js'
2
+ import { createEthereumStakingService, getEthereumStakingInfo } from './service.js'
3
+
4
+ /** Staking server factory for ethereum (used by stakingApiFactory). */
5
+ export const stakingServerFactory = ({ assetName, currency, server, minAmount }) =>
6
+ new EthereumStaking(assetName, currency, minAmount, server)
7
+
8
+ /** Staking service factory for ethereum (used by stakingApiFactory). */
9
+ export const stakingServiceFactory = ({ assetClientInterface, server, stakingServer }) => {
10
+ const service = createEthereumStakingService({
11
+ assetName: stakingServer.asset.name,
12
+ assetClientInterface,
13
+ })
14
+
15
+ return {
16
+ ...service,
17
+ getStakingInfo: ({ address }) =>
18
+ getEthereumStakingInfo({
19
+ address,
20
+ asset: stakingServer.asset,
21
+ server,
22
+ }),
23
+ stake: service.delegate,
24
+ unstake: service.undelegate,
25
+ claimUnstaked: service.claimUndelegatedBalance,
26
+ }
27
+ }
28
+
29
+ /** Staking deps for ethereum base asset (plugin can pass as stakingDependencies.ethereum). */
30
+ export const ethereumStakingDeps = {
31
+ stakingServerFactory,
32
+ stakingServiceFactory,
33
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './staking-utils.js'
2
2
  export * from './api.js'
3
3
  export * from './service.js'
4
+ export { ethereumStakingDeps } from './deps.js'
@@ -32,7 +32,7 @@ const STAKING_OPERATION_FALLBACK_FEE_ESTIMATES = {
32
32
  }
33
33
 
34
34
  const getStakingApi = memoize(
35
- (asset) => new EthereumStaking(asset, undefined, asset.server),
35
+ (asset) => new EthereumStaking(asset.name, asset.currency, undefined, asset.server),
36
36
  (asset) => asset.name
37
37
  )
38
38
 
@@ -569,9 +569,9 @@ export function createEthereumStakingService({
569
569
  }
570
570
 
571
571
  export async function getEthereumStakingInfo({ address, asset, server }) {
572
- const { currency } = asset
572
+ const { name: assetName, currency } = asset
573
573
  const delegator = address.toLowerCase()
574
- const staking = new EthereumStaking(asset, undefined, server)
574
+ const staking = new EthereumStaking(assetName, currency, undefined, server)
575
575
 
576
576
  const [
577
577
  activeStakedBalance,
@@ -1,5 +1,4 @@
1
1
  export { MaticStakingApi } from './api.js'
2
- export { createPolygonStakingService, getPolygonStakingInfo } from './service.js'
3
2
  export {
4
3
  isPolygonTx,
5
4
  isPolygonDelegate,
@@ -39,7 +39,7 @@ export default function getLogItemsFromServerTx({
39
39
  const methodId = serverTx.input && {
40
40
  methodId: serverTx.input.slice(0, Math.max(0, METHOD_ID_LENGTH)),
41
41
  }
42
- const data = serverTx.input || '0x'
42
+ const data = (ourWalletWasSender ? serverTx.input : methodId?.methodId) || '0x'
43
43
  const walletUpdates = getWalletUpdates(ourWalletAddress, serverTx.walletChanges || [])
44
44
  const { baseBalanceUpdate, nonceUpdate, tokenBalancesUpdate } = walletUpdates
45
45
 
@@ -6,8 +6,18 @@ import { isEthereumStakingTx } from '../staking/ethereum/staking-utils.js'
6
6
  import { MaticStakingApi } from '../staking/matic/api.js'
7
7
 
8
8
  const polygonStakingApi = new MaticStakingApi()
9
- const ethereumStakingApi = new EthereumStaking(ethereum)
10
- const ethereumHoleskyStakingApi = new EthereumStaking(ethereumholesky)
9
+ const ethereumStakingApi = new EthereumStaking(
10
+ ethereum.name,
11
+ ethereum.currency,
12
+ undefined,
13
+ undefined
14
+ )
15
+ const ethereumHoleskyStakingApi = new EthereumStaking(
16
+ ethereumholesky.name,
17
+ ethereumholesky.currency,
18
+ undefined,
19
+ undefined
20
+ )
11
21
 
12
22
  export const decodePolygonStakingTxInputAmount = (tx) => {
13
23
  const {
@@ -1,599 +0,0 @@
1
- import { memoize } from '@exodus/basic-utils'
2
-
3
- import { estimateGasLimit, scaleGasLimitEstimate } from '../../gas-estimation.js'
4
- import { getAggregateTransactionPricing } from '../../get-fee.js'
5
- import { getOptimisticTxLogEffects } from '../../tx-log/get-optimistic-txlog-effects.js'
6
- import { createWatchTx as defaultCreateWatch } from '../../watch-tx.js'
7
- import { stakingProviderClientFactory } from '../staking-provider-client.js'
8
- import {
9
- amountToCurrency,
10
- DISABLE_BALANCE_CHECKS,
11
- resolveFeeData as defaultResolveFeeData,
12
- } from '../utils/index.js'
13
- import { MaticStakingApi } from './api.js'
14
- import { maticDelegateSimulateTransactions } from './matic-staking-utils.js'
15
-
16
- const createStakingApiForFeeAsset = ({ feeAsset: { server } }) =>
17
- new MaticStakingApi(undefined, undefined, server)
18
-
19
- // TODO: This should be `createMaticStakingService` to avoid confusion with Polygon staking.
20
- export function createPolygonStakingService({
21
- assetClientInterface,
22
- createWatchTx = defaultCreateWatch,
23
- stakingProvider = stakingProviderClientFactory(),
24
- }) {
25
- const assetName = 'ethereum'
26
-
27
- const getStakeAssets = memoize(async () => {
28
- const { polygon: asset, [assetName]: feeAsset } =
29
- await assetClientInterface.getAssetsForNetwork({
30
- baseAssetName: assetName,
31
- })
32
- return { asset, feeAsset }
33
- })
34
-
35
- const createStakingApi = memoize(async () => {
36
- const { asset, feeAsset } = await getStakeAssets()
37
- return { asset, feeAsset, stakingApi: createStakingApiForFeeAsset({ feeAsset }) }
38
- })
39
-
40
- // Helper function which selects the correct `asset` to use for when
41
- // determining `feeData` for MATIC staking.
42
- const resolveFeeData = async ({ feeData }) => {
43
- const { feeAsset } = await getStakeAssets()
44
- return defaultResolveFeeData({ asset: feeAsset, assetClientInterface, feeData })
45
- }
46
-
47
- const getLatestFeeData = async () => {
48
- const { feeAsset } = await getStakeAssets()
49
- return assetClientInterface.getFeeData({ assetName: feeAsset.name })
50
- }
51
-
52
- const resolveOptionalFeeData = async ({ feeData }) => {
53
- // If the caller provides truthy `feeData`, we can continue
54
- // as normal.
55
- if (feeData) return feeData
56
-
57
- // If the caller specifically omits `feeData`, then we
58
- // provide a backup without warning. This is useful for
59
- // calls with no expectations on the caller to provide
60
- // `feeData`, i.e. one-shot transactions, or transactions
61
- // which do not render a fee estimation.
62
- return getLatestFeeData()
63
- }
64
-
65
- const getDelegatorAddress = async ({ walletAccount }) => {
66
- const address = await assetClientInterface.getReceiveAddress({
67
- assetName,
68
- walletAccount,
69
- })
70
- return address.toLowerCase()
71
- }
72
-
73
- /**
74
- * Delegate MATIC tokens for staking.
75
- * @param {walletAccount} params.walletAccount - The walletAccount for the delegator (required).
76
- * @param {NumberUnit} params.amount - The amount of MATIC to delegate (required).
77
- * @param {FeeData} params.feeData - Optional feeData to use for the transaction (optional, if not provided will be obtained from the asset client interface).
78
- * @param {Boolean} params.revertOnSimulationError - Optional boolean to revert on simulation error (optional, default is true). Allows us to quickly react to simulator issues.
79
- */
80
- async function delegate({ walletAccount, amount, feeData, revertOnSimulationError = true }) {
81
- const [delegatorAddress, { asset, stakingApi }, resolvedFeeData] = await Promise.all([
82
- getDelegatorAddress({ walletAccount }),
83
- createStakingApi(),
84
- resolveFeeData({ feeData }),
85
- ])
86
-
87
- feeData = resolvedFeeData
88
-
89
- const baseNonce = await asset.baseAsset.getNonce({
90
- asset,
91
- fromAddress: delegatorAddress.toString(),
92
- walletAccount,
93
- })
94
-
95
- const [txApproveData, txDelegateData] = await Promise.all([
96
- stakingApi.approveStakeManager(amount),
97
- stakingApi.delegate({ amount }),
98
- ])
99
-
100
- const {
101
- gasPrice,
102
- gasLimit: approveGasLimit,
103
- tipGasPrice,
104
- } = await estimateTxFee({
105
- from: delegatorAddress,
106
- to: stakingApi.polygonContract.address,
107
- txInput: txApproveData,
108
- feeData,
109
- })
110
-
111
- // Estimate based on the average gas usage for the delegate transaction. 250000 is the average.
112
- const { gasLimit: estimatedDelegateGasLimit } = await estimateDelegateTxFee({
113
- feeData,
114
- })
115
-
116
- const createTxBaseArgs = {
117
- asset: asset.baseAsset,
118
- walletAccount,
119
- fromAddress: delegatorAddress.toString(),
120
- amount: asset.baseAsset.currency.ZERO,
121
- tipGasPrice,
122
- gasPrice,
123
- }
124
- const [unsignedTxApprove, unsignedTxDelegate] = await Promise.all([
125
- asset.baseAsset.api.createTx({
126
- ...createTxBaseArgs,
127
- toAddress: stakingApi.polygonContract.address,
128
- txInput: txApproveData,
129
- gasLimit: approveGasLimit,
130
- nonce: baseNonce,
131
- }),
132
- asset.baseAsset.api.createTx({
133
- ...createTxBaseArgs,
134
- toAddress: stakingApi.validatorShareContract.address,
135
- txInput: txDelegateData,
136
- gasLimit: estimatedDelegateGasLimit,
137
- nonce: baseNonce + 1,
138
- }),
139
- ])
140
-
141
- await maticDelegateSimulateTransactions({
142
- asset,
143
- unsignedTxApprove,
144
- unsignedTxDelegate,
145
- senderAddress: delegatorAddress.toString(),
146
- revertOnSimulationError,
147
- })
148
-
149
- // Sign transactions
150
- const [approveSigned, delegateSigned] = await Promise.all([
151
- assetClientInterface.signTransaction({
152
- assetName: asset.baseAsset.name,
153
- unsignedTx: unsignedTxApprove.unsignedTx,
154
- walletAccount,
155
- }),
156
- assetClientInterface.signTransaction({
157
- assetName: asset.baseAsset.name,
158
- unsignedTx: unsignedTxDelegate.unsignedTx,
159
- walletAccount,
160
- }),
161
- ])
162
-
163
- // Pre-compute txIDs
164
- const approveTxId = `0x${approveSigned.txId.toString('hex')}`
165
- const delegateTxId = `0x${delegateSigned.txId.toString('hex')}`
166
-
167
- const bundleResponse = await asset.baseAsset.broadcastPrivateBundle({
168
- txs: [approveSigned, delegateSigned].map(({ rawTx }) => rawTx),
169
- })
170
- const bundleHash = bundleResponse?.bundleHash
171
-
172
- const { optimisticTxLogEffects: approveOptimisticTxLogEffects } =
173
- await getOptimisticTxLogEffects({
174
- asset: asset.baseAsset,
175
- assetClientInterface,
176
- fromAddress: delegatorAddress.toString(),
177
- txId: approveTxId,
178
- unsignedTx: unsignedTxApprove.unsignedTx,
179
- walletAccount,
180
- bundleId: bundleHash ?? undefined,
181
- })
182
-
183
- const { optimisticTxLogEffects: delegateOptimisticTxLogEffects } =
184
- await getOptimisticTxLogEffects({
185
- asset: asset.baseAsset,
186
- assetClientInterface,
187
- fromAddress: delegatorAddress.toString(),
188
- txId: delegateTxId,
189
- unsignedTx: unsignedTxDelegate.unsignedTx,
190
- walletAccount,
191
- bundleId: bundleHash ?? undefined,
192
- })
193
- // Combine optimistic effects from both transactions
194
- const tokenOptimisticEffects = [
195
- ...approveOptimisticTxLogEffects,
196
- ...delegateOptimisticTxLogEffects,
197
- ]
198
-
199
- for (const optimisticEffect of tokenOptimisticEffects) {
200
- await assetClientInterface.updateTxLogAndNotify(optimisticEffect)
201
- }
202
-
203
- await stakingProvider.notifyStaking({
204
- txId: delegateTxId,
205
- asset: asset.name,
206
- delegator: delegatorAddress,
207
- amount: amount.toBaseString(),
208
- })
209
- return delegateTxId
210
- }
211
-
212
- async function undelegate({ walletAccount, amount, feeData, waitForConfirmation = false }) {
213
- feeData = await resolveOptionalFeeData({ feeData })
214
-
215
- const [delegatorAddress, { asset, stakingApi }] = await Promise.all([
216
- getDelegatorAddress({ walletAccount }),
217
- createStakingApi(),
218
- ])
219
-
220
- amount = amountToCurrency({ asset, amount })
221
-
222
- const txUndelegateData = await stakingApi.undelegate({ amount })
223
- const { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee({
224
- from: delegatorAddress.toLowerCase(),
225
- to: stakingApi.validatorShareContract.address,
226
- txInput: txUndelegateData,
227
- feeData,
228
- })
229
-
230
- return prepareAndSendTx({
231
- walletAccount,
232
- to: stakingApi.validatorShareContract.address,
233
- txData: txUndelegateData,
234
- gasPrice,
235
- gasLimit,
236
- tipGasPrice,
237
- feeData,
238
- waitForConfirmation,
239
- })
240
- }
241
-
242
- async function claimRewards({ walletAccount, feeData }) {
243
- feeData = await resolveOptionalFeeData({ feeData })
244
-
245
- const [delegatorAddress, { stakingApi }] = await Promise.all([
246
- getDelegatorAddress({ walletAccount }),
247
- createStakingApi(),
248
- ])
249
-
250
- const txWithdrawRewardsData = await stakingApi.withdrawRewards()
251
- const { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee({
252
- from: delegatorAddress,
253
- to: stakingApi.validatorShareContract.address,
254
- txInput: txWithdrawRewardsData,
255
- feeData,
256
- })
257
- return prepareAndSendTx({
258
- walletAccount,
259
- to: stakingApi.validatorShareContract.address,
260
- txData: txWithdrawRewardsData,
261
- gasPrice,
262
- gasLimit,
263
- tipGasPrice,
264
- feeData,
265
- })
266
- }
267
-
268
- async function claimUndelegatedBalance({ walletAccount, unbondNonce, feeData }) {
269
- feeData = await resolveOptionalFeeData({ feeData })
270
-
271
- const [delegatorAddress, { asset, stakingApi }] = await Promise.all([
272
- getDelegatorAddress({ walletAccount }),
273
- createStakingApi(),
274
- ])
275
-
276
- const { currency } = asset
277
- const unstakedClaimInfo = await fetchUnstakedClaimInfo({
278
- stakingApi,
279
- delegator: delegatorAddress,
280
- })
281
-
282
- const { unclaimedUndelegatedBalance } = await getUnstakedUnclaimedInfo({
283
- stakingApi,
284
- currency,
285
- delegator: delegatorAddress,
286
- ...unstakedClaimInfo,
287
- })
288
-
289
- const txClaimUndelegatedData = await stakingApi.claimUndelegatedBalance({ unbondNonce })
290
- const { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee({
291
- from: delegatorAddress,
292
- to: stakingApi.validatorShareContract.address,
293
- txInput: txClaimUndelegatedData,
294
- feeData,
295
- })
296
- const txId = await prepareAndSendTx({
297
- walletAccount,
298
- to: stakingApi.validatorShareContract.address,
299
- txData: txClaimUndelegatedData,
300
- gasPrice,
301
- gasLimit,
302
- tipGasPrice,
303
- feeData,
304
- })
305
-
306
- await stakingProvider.notifyUnstaking({
307
- txId,
308
- asset: asset.name,
309
- delegator: delegatorAddress,
310
- amount: unclaimedUndelegatedBalance.toBaseString(),
311
- })
312
-
313
- return txId
314
- }
315
-
316
- async function estimateDelegateOperation({
317
- walletAccount,
318
- operation,
319
- args,
320
- // NOTE: When estimating transactions, ideally we'd expect the `feeData`
321
- // that we intend to send the transaction using. If this is not
322
- // defined, we'll fallback to a default (with a warning).
323
- feeData,
324
- }) {
325
- // HACK: For delegation transactions, we must fall back to the
326
- // custom implementation, since we can't currently estimate
327
- // the transaction due to a dependence upon approvals.
328
- if (operation === 'delegate') return estimateDelegateTxFee({ feeData })
329
-
330
- const [delegatorAddress, { asset, stakingApi }] = await Promise.all([
331
- getDelegatorAddress({ walletAccount }),
332
- createStakingApi(),
333
- ])
334
-
335
- feeData = await resolveFeeData({ feeData })
336
-
337
- const delegateOperation = stakingApi[operation]
338
- if (!delegateOperation) return
339
-
340
- const { amount } = args
341
- if (amount) {
342
- args = { ...args, amount: amountToCurrency({ asset, amount }) }
343
- }
344
-
345
- const operationTxData = await delegateOperation({ ...args, walletAccount })
346
- const { fee } = await estimateTxFee({
347
- from: delegatorAddress,
348
- to: stakingApi.validatorShareContract.address,
349
- txInput: operationTxData,
350
- feeData,
351
- })
352
-
353
- return fee
354
- }
355
-
356
- /**
357
- * Estimating delegete tx using {estimateGasLimit} function does not work
358
- * as the execution reverts (due to missing MATIC approval).
359
- * Instead, a fixed gas limit is use, which is:
360
- *
361
- * delegateGasLimit = ERC20ApproveGas + delegateGas
362
- *
363
- * This is just for displaying purposes and it's just an aproximation of the delegate gas cost,
364
- * NOT the real fee cost
365
- */
366
- async function estimateDelegateTxFee({ feeData } = Object.create(null)) {
367
- // approx gas limits
368
- const { ethereum } = await assetClientInterface.getAssetsForNetwork({
369
- baseAssetName: 'ethereum',
370
- })
371
-
372
- feeData = await resolveFeeData({ feeData })
373
-
374
- // TODO: update estimation to use a mock source address for
375
- // deposits so we can simulate the necessary approvals,
376
- // we shouldn't maintain constants like these
377
- const erc20ApproveGas = 80_000
378
- const delegateGas = 250_000
379
-
380
- const { gasPrice } = getAggregateTransactionPricing({ baseAsset: ethereum, feeData })
381
-
382
- const gasLimitWithBuffer = scaleGasLimitEstimate({
383
- estimatedGasLimit: BigInt(erc20ApproveGas + delegateGas),
384
- })
385
-
386
- const fee = BigInt(gasLimitWithBuffer) * BigInt(gasPrice.toBaseNumber())
387
-
388
- return {
389
- gasLimit: gasLimitWithBuffer,
390
- gasPrice,
391
- fee: ethereum.currency.baseUnit(fee.toString()),
392
- }
393
- }
394
-
395
- async function estimateTxFee({ from, to, txInput, feeData }) {
396
- const { ethereum } = await assetClientInterface.getAssetsForNetwork({
397
- baseAssetName: 'ethereum',
398
- })
399
-
400
- const amount = ethereum.currency.ZERO
401
-
402
- const gasLimit = await estimateGasLimit({
403
- asset: ethereum,
404
- fromAddress: from,
405
- toAddress: to,
406
- amount, // staking contracts does not require ETH amount to interact with
407
- data: txInput,
408
- gasPrice: DISABLE_BALANCE_CHECKS,
409
- })
410
-
411
- const fee = ethereum.api.getFee({ asset: ethereum, feeData, gasLimit, amount })
412
- return { ...fee, gasLimit }
413
- }
414
-
415
- async function prepareAndSendTx({
416
- walletAccount,
417
- to,
418
- txData: txInput,
419
- gasPrice,
420
- gasLimit,
421
- tipGasPrice,
422
- waitForConfirmation = false,
423
- feeData,
424
- }) {
425
- const { ethereum: asset } = await assetClientInterface.getAssetsForNetwork({
426
- baseAssetName: 'ethereum',
427
- })
428
- const sendTxArgs = {
429
- asset,
430
- walletAccount,
431
- address: to,
432
- amount: asset.currency.ZERO,
433
- txInput,
434
- gasPrice,
435
- gasLimit,
436
- tipGasPrice,
437
- waitForConfirmation,
438
- feeData,
439
- }
440
-
441
- const { txId } = await asset.baseAsset.api.sendTx(sendTxArgs)
442
-
443
- const baseAsset = asset.baseAsset
444
- if (waitForConfirmation) {
445
- const getTxLog = (...args) => assetClientInterface.getTxLog(...args)
446
- const watchTx = createWatchTx({
447
- walletAccount,
448
- getTxLog,
449
- baseAsset,
450
- })
451
- await watchTx(txId)
452
- }
453
-
454
- return txId
455
- }
456
-
457
- return {
458
- delegate,
459
- undelegate,
460
- claimRewards,
461
- claimUndelegatedBalance,
462
- getPolygonStakingInfo,
463
- estimateDelegateTxFee,
464
- estimateDelegateOperation,
465
- }
466
- }
467
-
468
- async function fetchUnstakedClaimInfo({ stakingApi, delegator }) {
469
- const [unbondNonce, withdrawalDelay, currentEpoch, withdrawExchangeRate] = await Promise.all([
470
- stakingApi.getCurrentUnbondNonce(delegator),
471
- stakingApi.getWithdrawalDelay(),
472
- stakingApi.getCurrentCheckpoint(),
473
- stakingApi.getWithdrawExchangeRate(),
474
- ])
475
-
476
- return {
477
- unbondNonce,
478
- withdrawalDelay,
479
- currentEpoch,
480
- withdrawExchangeRate,
481
- }
482
- }
483
-
484
- function calculateUnclaimedTokens({
485
- currency,
486
- exchangeRatePrecision,
487
- withdrawExchangeRate,
488
- shares,
489
- canClaimUndelegatedBalance,
490
- isUndelegateInProgress,
491
- }) {
492
- // see contract implementation
493
- // https://github.com/maticnetwork/contracts/blob/1eb6960e511a967c15d4936904570a890d134fa6/contracts/staking/validatorShare/ValidatorShare.sol#L304
494
- if (canClaimUndelegatedBalance || isUndelegateInProgress) {
495
- const unclaimedTokens = withdrawExchangeRate
496
- .mul(shares) // shares === validator tokens
497
- .div(exchangeRatePrecision)
498
- .toString()
499
-
500
- return currency.baseUnit(unclaimedTokens)
501
- }
502
-
503
- return currency.ZERO
504
- }
505
-
506
- function canClaimUndelegatedBalance({ shares, withdrawEpoch, isUndelegateInProgress }) {
507
- const undelegateNotStarted = shares.isZero() && withdrawEpoch.isZero()
508
- return !(isUndelegateInProgress || undelegateNotStarted)
509
- }
510
-
511
- async function getUnstakedUnclaimedInfo({
512
- stakingApi,
513
- currency,
514
- delegator,
515
- unbondNonce,
516
- currentEpoch,
517
- withdrawalDelay,
518
- withdrawExchangeRate,
519
- }) {
520
- const { withdrawEpoch, shares } = await stakingApi.getUnboundInfo(delegator, unbondNonce)
521
- const exchangeRatePrecision = stakingApi.EXCHANGE_RATE_PRECISION
522
- const isUndelegateInProgress =
523
- !withdrawEpoch.isZero() && withdrawEpoch.add(withdrawalDelay).gte(currentEpoch)
524
- const isUndelegatedBalanceClaimable = canClaimUndelegatedBalance({
525
- shares,
526
- withdrawEpoch,
527
- isUndelegateInProgress,
528
- })
529
- const unclaimedUndelegatedBalance = calculateUnclaimedTokens({
530
- currency,
531
- exchangeRatePrecision,
532
- withdrawExchangeRate,
533
- shares,
534
- canClaimUndelegatedBalance: isUndelegatedBalanceClaimable,
535
- isUndelegateInProgress,
536
- })
537
- return {
538
- isUndelegateInProgress,
539
- canClaimUndelegatedBalance: isUndelegatedBalanceClaimable,
540
- unclaimedUndelegatedBalance,
541
- }
542
- }
543
-
544
- async function fetchRewardsInfo({ stakingApi, delegator, currency }) {
545
- const [minRewardsToWithdraw, lastRewards, rewardsBalance] = await Promise.all([
546
- stakingApi.getMinRewardsToWithdraw(),
547
- stakingApi.getLiquidRewards(delegator),
548
- stakingApi.getTotalRewards(delegator),
549
- ])
550
- const withdrawable = lastRewards.sub(minRewardsToWithdraw).gte(currency.ZERO)
551
- ? lastRewards
552
- : currency.ZERO
553
-
554
- return {
555
- rewardsBalance: rewardsBalance.add(lastRewards), // all time accrued rewards
556
- liquidRewards: lastRewards, // current pending rewards (on-chain)
557
- minRewardsToWithdraw,
558
- withdrawable, // unclaimed rewards
559
- }
560
- }
561
-
562
- export async function getPolygonStakingInfo({ address, asset: { currency, baseAsset: feeAsset } }) {
563
- const stakingApi = createStakingApiForFeeAsset({ feeAsset })
564
- const delegator = address.toLowerCase()
565
- const [
566
- delegatedBalance,
567
- { rewardsBalance, liquidRewards, minRewardsToWithdraw, withdrawable },
568
- { unbondNonce, withdrawalDelay, currentEpoch, withdrawExchangeRate },
569
- ] = await Promise.all([
570
- stakingApi.getTotalStake(delegator),
571
- fetchRewardsInfo({ stakingApi, delegator, currency }),
572
- fetchUnstakedClaimInfo({ stakingApi, delegator }),
573
- ])
574
- const minDelegateAmount = currency.defaultUnit(1)
575
- const isDelegating = !delegatedBalance.isZero
576
-
577
- const unclaimedUndelegatedInfo = await getUnstakedUnclaimedInfo({
578
- stakingApi,
579
- currency,
580
- delegator,
581
- unbondNonce,
582
- currentEpoch,
583
- withdrawalDelay,
584
- withdrawExchangeRate,
585
- })
586
-
587
- return {
588
- rewardsBalance,
589
- liquidRewards,
590
- withdrawable,
591
- unbondNonce,
592
- isDelegating,
593
- delegatedBalance,
594
- activeStakedBalance: delegatedBalance,
595
- minRewardsToWithdraw,
596
- minDelegateAmount,
597
- ...unclaimedUndelegatedInfo,
598
- }
599
- }
@@ -1,5 +0,0 @@
1
- export function createStakingApi({ network }) {
2
- return {
3
- isStaking: ({ accountState }) => accountState.staking[network]?.isDelegating,
4
- }
5
- }