@exodus/ethereum-api 8.59.2 → 8.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,34 @@
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.61.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.60.0...@exodus/ethereum-api@8.61.0) (2025-12-05)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: asset family (#6984)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+
18
+ * fix: register `eth_estimateGas` as a critical section (#7057)
19
+
20
+
21
+
22
+ ## [8.60.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.59.2...@exodus/ethereum-api@8.60.0) (2025-11-22)
23
+
24
+
25
+ ### Features
26
+
27
+
28
+ * feat: Bundle approve and delegate functions for Matic staking (With simulation) (#6707)
29
+
30
+
31
+ * feat: Default privacyRpcUrl (SERVO) (#6968)
32
+
33
+
6
34
  ## [8.59.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.59.1...@exodus/ethereum-api@8.59.2) (2025-11-19)
7
35
 
8
36
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.59.2",
3
+ "version": "8.61.0",
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",
@@ -29,7 +29,7 @@
29
29
  "@exodus/bip44-constants": "^195.0.0",
30
30
  "@exodus/crypto": "^1.0.0-rc.26",
31
31
  "@exodus/currency": "^6.0.1",
32
- "@exodus/ethereum-lib": "^5.19.0",
32
+ "@exodus/ethereum-lib": "^5.20.0",
33
33
  "@exodus/ethereum-meta": "^2.9.1",
34
34
  "@exodus/ethereumholesky-meta": "^2.0.5",
35
35
  "@exodus/ethereumjs": "^1.8.0",
@@ -38,7 +38,7 @@
38
38
  "@exodus/safe-string": "^1.2.1",
39
39
  "@exodus/simple-retry": "^0.0.6",
40
40
  "@exodus/solidity-contract": "^1.3.0",
41
- "@exodus/web3-ethereum-utils": "^4.5.1",
41
+ "@exodus/web3-ethereum-utils": "^4.6.0",
42
42
  "bn.js": "^5.2.1",
43
43
  "delay": "^4.0.1",
44
44
  "eventemitter3": "^4.0.7",
@@ -67,5 +67,5 @@
67
67
  "type": "git",
68
68
  "url": "git+https://github.com/ExodusMovement/assets.git"
69
69
  },
70
- "gitHead": "d1505076f4e06aad8f7962d9fad12354c020a30c"
70
+ "gitHead": "bb5b646c92ab70a07c2bae1fd9e717a9ee8d2071"
71
71
  }
@@ -61,7 +61,7 @@ export const resolveMonitorSettings = (
61
61
  return { ...defaultResolution, monitorType: overrideMonitorType, serverUrl: overrideServerUrl }
62
62
  }
63
63
 
64
- const stringifyPrivateTx = (tx) => {
64
+ export const stringifyPrivateTx = (tx) => {
65
65
  assert(tx, 'expected tx')
66
66
  if (tx instanceof Uint8Array) return `0x${Buffer.from(tx).toString('hex')}`
67
67
 
@@ -75,14 +75,21 @@ const broadcastPrivateBundleFactory =
75
75
  ({ privacyServer }) =>
76
76
  async ({ txs }) => {
77
77
  assert(Array.isArray(txs), 'txs must be an array')
78
- if (txs.length === 0) return
78
+ assert(txs.length > 0, 'txs must be an non-empty array')
79
79
 
80
- await privacyServer.sendRequest(
80
+ const sendBundleResult = await privacyServer.sendRequest(
81
81
  privacyServer.buildRequest({
82
82
  method: 'eth_sendBundle',
83
83
  params: [{ txs: txs.map((tx) => stringifyPrivateTx(tx)) }],
84
84
  })
85
85
  )
86
+ if (typeof sendBundleResult === 'string') return { bundleHash: sendBundleResult }
87
+
88
+ const bundleHash = sendBundleResult?.bundleHash
89
+ if (typeof bundleHash === 'string') return { bundleHash }
90
+
91
+ console.warn('unexpected sendBundleResult shape, cannot determine bundle hash')
92
+ return null
86
93
  }
87
94
 
88
95
  export const createTransactionPrivacyFactory = ({ assetName, privacyRpcUrl }) => {
@@ -1,4 +1,4 @@
1
- import { connectAssetsList } from '@exodus/assets'
1
+ import { ASSET_FAMILY, connectAssetsList } from '@exodus/assets'
2
2
  import bip44Constants from '@exodus/bip44-constants/by-ticker.js'
3
3
  import {
4
4
  createEthereumLikeAccountState,
@@ -69,6 +69,7 @@ export const createAssetFactory = ({
69
69
  supportsCustomFees: defaultSupportsCustomFees = false,
70
70
  useAbsoluteBalanceAndNonce = false,
71
71
  delisted = false,
72
+ privacyRpcUrl: defaultPrivacyRpcUrl,
72
73
  }) => {
73
74
  assert(assetsList, 'assetsList is required')
74
75
  assert(providedFeeData || feeDataConfig, 'feeData or feeDataConfig is required')
@@ -91,6 +92,7 @@ export const createAssetFactory = ({
91
92
  supportsCustomFees: defaultSupportsCustomFees,
92
93
  nfts: defaultNfts,
93
94
  customTokens: defaultCustomTokens,
95
+ privacyRpcUrl: defaultPrivacyRpcUrl,
94
96
  }
95
97
  return (
96
98
  {
@@ -180,6 +182,7 @@ export const createAssetFactory = ({
180
182
  const features = {
181
183
  accountState: true,
182
184
  customTokens,
185
+ family: ASSET_FAMILY.EVM,
183
186
  feeMonitor: true,
184
187
  feesApi: true,
185
188
  isMaxFeeAsset,
@@ -1,3 +1,4 @@
1
+ import { ASSET_FAMILY } from '@exodus/assets'
1
2
  import { createContract } from '@exodus/ethereum-lib'
2
3
  import assert from 'minimalistic-assert'
3
4
 
@@ -9,6 +10,7 @@ import {
9
10
  import { createStakingApi } from './staking-api.js'
10
11
 
11
12
  const defaultTokenFeatures = {
13
+ family: ASSET_FAMILY.EVM,
12
14
  isMaxFeeAsset: true,
13
15
  }
14
16
 
@@ -216,4 +216,12 @@ export default class ClarityServerV2 extends ClarityServer {
216
216
  await this.fetchRpcHttpRequest({ baseApiPath, body: request })
217
217
  )
218
218
  }
219
+
220
+ async estimateGas(...params) {
221
+ const { baseApiPath } = this
222
+ const request = this.estimateGasRequest(...params)
223
+ return this.handleJsonRPCResponse(
224
+ await this.fetchRpcHttpRequest({ baseApiPath, body: request })
225
+ )
226
+ }
219
227
  }
@@ -60,7 +60,7 @@ export function createEthereumStakingService({
60
60
  return { delegatorAddress, feeData }
61
61
  }
62
62
 
63
- async function delegate({ walletAccount, amount, feeData, isDelegateAll } = Object.create(null)) {
63
+ async function delegate({ walletAccount, amount, feeData, isDelegateAll }) {
64
64
  const asset = await getAsset(assetName)
65
65
  const staking = getStakingApi(asset)
66
66
  amount = amountToCurrency({ asset, amount })
@@ -230,9 +230,7 @@ export function createEthereumStakingService({
230
230
  return undelegatePendingFee.add(undelegateFee)
231
231
  }
232
232
 
233
- async function undelegate(
234
- { walletAccount, amount, feeData, waitForConfirmation = true } = Object.create(null)
235
- ) {
233
+ async function undelegate({ walletAccount, amount, feeData, waitForConfirmation = true }) {
236
234
  const asset = await getAsset(assetName)
237
235
  const staking = getStakingApi(asset)
238
236
  const minAmount = getMinAmount(asset)
@@ -312,7 +310,7 @@ export function createEthereumStakingService({
312
310
  return txId
313
311
  }
314
312
 
315
- async function claimUndelegatedBalance({ walletAccount, feeData } = Object.create(null)) {
313
+ async function claimUndelegatedBalance({ walletAccount, feeData }) {
316
314
  const asset = await getAsset(assetName)
317
315
  const staking = getStakingApi(asset)
318
316
 
@@ -495,20 +493,18 @@ export function createEthereumStakingService({
495
493
  }
496
494
  }
497
495
 
498
- async function prepareAndSendTx(
499
- {
500
- asset,
501
- walletAccount,
502
- to,
503
- amount,
504
- txData: txInput,
505
- gasPrice,
506
- gasLimit,
507
- tipGasPrice,
508
- waitForConfirmation = false,
509
- feeData,
510
- } = Object.create(null)
511
- ) {
496
+ async function prepareAndSendTx({
497
+ asset,
498
+ walletAccount,
499
+ to,
500
+ amount,
501
+ txData: txInput,
502
+ gasPrice,
503
+ gasLimit,
504
+ tipGasPrice,
505
+ waitForConfirmation = false,
506
+ feeData,
507
+ }) {
512
508
  const sendTxArgs = {
513
509
  asset,
514
510
  walletAccount,
@@ -1,10 +1,14 @@
1
+ import NumberUnit from '@exodus/currency'
2
+ import { parseUnsignedTx } from '@exodus/ethereum-lib'
1
3
  import assetsList, { asset as ethereum } from '@exodus/ethereum-meta'
2
4
  import { Tx } from '@exodus/models'
5
+ import assert from 'minimalistic-assert'
3
6
 
7
+ import { decodePolygonStakingTxInputAmount } from '../../tx-log-staking-processor/utils.js'
4
8
  import { MaticStakingApi } from './api.js'
5
9
 
6
10
  // function selector for buyVoucher(uint256 _amount, uint256 _minSharesToMint)
7
- const DELEGATE = '0x6ab15071'
11
+ export const DELEGATE = '0x6ab15071'
8
12
  // function selector for sellVoucher_new(uint256 claimAmount, uint256 maximumSharesToBurn)
9
13
  const UNDELEGATE = '0xc83ec04d'
10
14
  // function selector for withdrawRewards()
@@ -76,3 +80,170 @@ export const getPolygonUndelegateTxInEthereumTxLog = (ethereumLogItems) => {
76
80
 
77
81
  return polygonLogItems
78
82
  }
83
+
84
+ // Build simulation transaction requests from unsigned transactions
85
+ export function createMaticStakeSimRequestsFromUnsigned({
86
+ asset,
87
+ unsignedTxApprove,
88
+ unsignedTxDelegate,
89
+ }) {
90
+ const build = (tx) => {
91
+ const parsed = parseUnsignedTx({ asset, unsignedTx: tx.unsignedTx })
92
+ const gasPriceUnit = tx.gasPrice
93
+ const chainId = tx.unsignedTx?.txData?.chainId
94
+ const from = tx.unsignedTx?.txMeta?.fromAddress
95
+ const to = tx.unsignedTx?.txMeta?.toAddress
96
+
97
+ assert(gasPriceUnit instanceof NumberUnit, 'expected NumberUnit gasPrice')
98
+ assert(Number.isInteger(chainId), 'expected integer chainId')
99
+ assert(typeof from === 'string' && from, 'expected string fromAddress')
100
+ assert(typeof to === 'string' && to, 'expected string toAddress')
101
+
102
+ return {
103
+ chainId,
104
+ from,
105
+ to,
106
+ gas: `0x${parsed.gasLimit.toString(16)}`,
107
+ gasPrice: `0x${BigInt(gasPriceUnit.toBaseString()).toString(16)}`,
108
+ value: '0x0',
109
+ data: `0x${parsed.data?.toString('hex')}`,
110
+ nonce: `0x${parsed.nonce.toString(16)}`,
111
+ }
112
+ }
113
+
114
+ return {
115
+ approveTxRequest: build(unsignedTxApprove),
116
+ delegateTxRequest: build(unsignedTxDelegate),
117
+ }
118
+ }
119
+
120
+ export async function maticDelegateOptimisticSideEffectTxLogs({
121
+ asset, // Base asset
122
+ walletAccount,
123
+ feeAmount,
124
+ delegateTxId,
125
+ nonce,
126
+ estimatedDelegateGasLimit,
127
+ tipGasPrice,
128
+ transactionData,
129
+ date,
130
+ bundleId,
131
+ }) {
132
+ // Decode the amount from the delegate transaction data
133
+ // transactionData is already parsed from unsignedTx using parseUnsignedTx
134
+ let amount
135
+ try {
136
+ // Decode the contract call data - buyVoucher(uint256 _amount, uint256 _minSharesToMint)
137
+ const decodedAmount = decodePolygonStakingTxInputAmount({ data: { data: transactionData } })
138
+ // Convert to NumberUnit - the decoded amount is a string like '100000000'
139
+ amount = polygonCurrency.baseUnit(decodedAmount)
140
+ } catch (e) {
141
+ console.error(
142
+ 'Could not decode MATIC delegate transaction data:',
143
+ delegateTxId,
144
+ e.message,
145
+ transactionData
146
+ )
147
+ return [] // Could not decode the amount
148
+ }
149
+
150
+ const tokenAssetName = 'polygon'
151
+ return [
152
+ {
153
+ assetName: tokenAssetName,
154
+ walletAccount,
155
+ txs: [
156
+ {
157
+ confirmations: 0,
158
+ feeAmount,
159
+ feeCoinName: asset.feeAsset.name,
160
+ selfSend: false,
161
+ to: MaticStakingApi.EVERSTAKE_VALIDATOR_CONTRACT_ADDR,
162
+ txId: delegateTxId,
163
+ data: {
164
+ gasLimit: estimatedDelegateGasLimit,
165
+ nonce,
166
+ tipGasPrice: tipGasPrice ? tipGasPrice.toBaseString() : undefined,
167
+ methodId: DELEGATE,
168
+ ...(bundleId ? { bundleId } : null),
169
+ },
170
+ coinAmount: amount.abs().negate(),
171
+ coinName: tokenAssetName,
172
+ currencies: {
173
+ [tokenAssetName]: polygonCurrency,
174
+ [asset.feeAsset.name]: asset.feeAsset.currency,
175
+ },
176
+ date,
177
+ },
178
+ ],
179
+ },
180
+ ]
181
+ }
182
+
183
+ export async function maticDelegateSimulateTransactions({
184
+ asset,
185
+ unsignedTxApprove,
186
+ unsignedTxDelegate,
187
+ senderAddress,
188
+ revertOnSimulationError,
189
+ }) {
190
+ const { approveTxRequest, delegateTxRequest } = createMaticStakeSimRequestsFromUnsigned({
191
+ asset: asset.baseAsset,
192
+ unsignedTxApprove,
193
+ unsignedTxDelegate,
194
+ })
195
+
196
+ // Simulate transactions
197
+ const simulationTxData = {
198
+ baseAssetName: asset.baseAsset.name,
199
+ origin: 'exodus-staking',
200
+ senderAddress,
201
+ }
202
+
203
+ const simulationResponse = await asset.baseAsset.api.web3.simulateTransactions({
204
+ ...simulationTxData,
205
+ transactions: [approveTxRequest, delegateTxRequest],
206
+ })
207
+ // These checks are only for simulation issues, not validation of the transactions
208
+ if (simulationResponse?.warnings?.length || simulationResponse?.metadata?.humanReadableError) {
209
+ if (revertOnSimulationError) {
210
+ const err = new Error('StakingMaticSimulationError')
211
+ err.message = simulationResponse?.metadata?.humanReadableError
212
+ err.reason = `warnings: ${simulationResponse.warnings.join(', ')}`
213
+ err.hint = 'delegate-matic-simulation-error'
214
+ throw err
215
+ } else {
216
+ console.warn(
217
+ 'Simulation for staking matic returned issues',
218
+ simulationResponse?.metadata?.humanReadableError
219
+ )
220
+ }
221
+ }
222
+
223
+ // For validation of the transactions, we need to check if the simulation produced the expected effects
224
+ // hasApprove, hasSend, hasReceive entries must be present
225
+ const { balanceChanges = Object.create(null) } = simulationResponse || Object.create(null)
226
+ const asArray = (v) => (Array.isArray(v) ? v : [])
227
+ const hasApprove = asArray(balanceChanges.willApprove).length > 0
228
+ const hasSend = asArray(balanceChanges.willSend).length > 0
229
+ const hasReceive = asArray(balanceChanges.willReceive).length > 0
230
+
231
+ if (!(hasApprove && hasSend && hasReceive)) {
232
+ if (revertOnSimulationError) {
233
+ const missing = []
234
+ if (!hasApprove) missing.push('willApprove')
235
+ if (!hasSend) missing.push('willSend')
236
+ if (!hasReceive) missing.push('willReceive')
237
+ const err = new Error('StakingMaticSimulationResultsError')
238
+ err.message = 'Simulation did not produce expected effects'
239
+ err.reason = `missing balanceChanges: ${missing.join(', ')}`
240
+ err.hint = 'delegate-matic-simulation-results-error'
241
+ throw err
242
+ } else {
243
+ console.warn(
244
+ 'Simulation for staking matic returned issues',
245
+ simulationResponse?.metadata?.humanReadableError
246
+ )
247
+ }
248
+ }
249
+ }
@@ -2,6 +2,7 @@ import { memoize } from '@exodus/basic-utils'
2
2
 
3
3
  import { estimateGasLimit, scaleGasLimitEstimate } from '../../gas-estimation.js'
4
4
  import { getAggregateTransactionPricing } from '../../get-fee.js'
5
+ import { getOptimisticTxLogEffects } from '../../tx-log/get-optimistic-txlog-effects.js'
5
6
  import { createWatchTx as defaultCreateWatch } from '../../watch-tx.js'
6
7
  import { stakingProviderClientFactory } from '../staking-provider-client.js'
7
8
  import {
@@ -10,6 +11,7 @@ import {
10
11
  resolveFeeData as defaultResolveFeeData,
11
12
  } from '../utils/index.js'
12
13
  import { MaticStakingApi } from './api.js'
14
+ import { maticDelegateSimulateTransactions } from './matic-staking-utils.js'
13
15
 
14
16
  const createStakingApiForFeeAsset = ({ feeAsset: { server } }) =>
15
17
  new MaticStakingApi(undefined, undefined, server)
@@ -68,86 +70,146 @@ export function createPolygonStakingService({
68
70
  return address.toLowerCase()
69
71
  }
70
72
 
71
- async function delegate({ walletAccount, amount, feeData, waitForConfirmation = false } = {}) {
72
- const [delegatorAddress, { asset, stakingApi }] = await Promise.all([
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([
73
82
  getDelegatorAddress({ walletAccount }),
74
83
  createStakingApi(),
84
+ resolveFeeData({ feeData }),
75
85
  ])
76
86
 
77
- // NOTE: We do not provide a silent fallback for `feeData` omission
78
- // for a delegation transaction, since `feeData` is expected
79
- // to have been provided during `estimateDelegateTxFee`.
80
- feeData = await resolveFeeData({ feeData })
87
+ feeData = resolvedFeeData
81
88
 
82
- amount = amountToCurrency({ asset, amount })
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
+ ])
83
99
 
84
- const txApproveData = await stakingApi.approveStakeManager(amount)
85
- let { gasPrice, gasLimit, tipGasPrice } = await estimateTxFee({
100
+ const {
101
+ gasPrice,
102
+ gasLimit: approveGasLimit,
103
+ tipGasPrice,
104
+ } = await estimateTxFee({
86
105
  from: delegatorAddress,
87
106
  to: stakingApi.polygonContract.address,
88
107
  txInput: txApproveData,
89
108
  feeData,
90
109
  })
91
- await prepareAndSendTx({
92
- walletAccount,
93
- waitForConfirmation,
94
- to: stakingApi.polygonContract.address,
95
- txData: txApproveData,
96
- gasPrice,
97
- gasLimit,
98
- tipGasPrice,
110
+
111
+ // Estimate based on the average gas usage for the delegate transaction. 250000 is the average.
112
+ const { gasLimit: estimatedDelegateGasLimit } = await estimateDelegateTxFee({
99
113
  feeData,
100
114
  })
101
115
 
102
- // NOTE: Whenever we `waitForConfirmation`, we must recalculate
103
- // the `feeData` as this may have changed since our last
104
- // transaction was mined.
105
- //
106
- // In most cases, this shouldn't affect previous estimates,
107
- // since the amount we aren't trying to stake using the
108
- // base currency; however for users with marginal balances,
109
- // an increase in `gasPrice` could result in transaction
110
- // failure.
111
- //
112
- // HACK: This will invaldiate the original estimate in calculated
113
- // in `estimateDelegateTxFee`!
114
- // TODO: Submit these transactions as an atomic bundle.
115
- if (waitForConfirmation) {
116
- feeData = await getLatestFeeData()
116
+ const createTxBaseArgs = {
117
+ asset: asset.baseAsset,
118
+ walletAccount,
119
+ fromAddress: delegatorAddress.toString(),
120
+ amount: asset.baseAsset.currency.ZERO,
121
+ tipGasPrice,
122
+ gasPrice,
117
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
+ })
118
148
 
119
- const txDelegateData = await stakingApi.delegate({ amount })
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
+ ])
120
162
 
121
- ;({ gasPrice, gasLimit, tipGasPrice } = await estimateTxFee({
122
- from: delegatorAddress,
123
- to: stakingApi.validatorShareContract.address,
124
- txInput: txDelegateData,
125
- feeData,
126
- }))
163
+ // Pre-compute txIDs
164
+ const approveTxId = `0x${approveSigned.txId.toString('hex')}`
165
+ const delegateTxId = `0x${delegateSigned.txId.toString('hex')}`
127
166
 
128
- const txId = await prepareAndSendTx({
129
- walletAccount,
130
- // TOOD: Why shouldn't we wait for confirmation here? Is it because we want to `notifyStaking`?
131
- waitForConfirmation: false,
132
- to: stakingApi.validatorShareContract.address,
133
- txData: txDelegateData,
134
- gasPrice,
135
- gasLimit,
136
- tipGasPrice,
137
- feeData,
167
+ const bundleResponse = await asset.baseAsset.broadcastPrivateBundle({
168
+ txs: [approveSigned, delegateSigned].map(({ rawTx }) => rawTx),
138
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
+ }
139
202
 
140
203
  await stakingProvider.notifyStaking({
141
- txId,
204
+ txId: delegateTxId,
142
205
  asset: asset.name,
143
206
  delegator: delegatorAddress,
144
207
  amount: amount.toBaseString(),
145
208
  })
146
-
147
- return txId
209
+ return delegateTxId
148
210
  }
149
211
 
150
- async function undelegate({ walletAccount, amount, feeData, waitForConfirmation = false } = {}) {
212
+ async function undelegate({ walletAccount, amount, feeData, waitForConfirmation = false }) {
151
213
  feeData = await resolveOptionalFeeData({ feeData })
152
214
 
153
215
  const [delegatorAddress, { asset, stakingApi }] = await Promise.all([
@@ -177,7 +239,7 @@ export function createPolygonStakingService({
177
239
  })
178
240
  }
179
241
 
180
- async function claimRewards({ walletAccount, feeData } = {}) {
242
+ async function claimRewards({ walletAccount, feeData }) {
181
243
  feeData = await resolveOptionalFeeData({ feeData })
182
244
 
183
245
  const [delegatorAddress, { stakingApi }] = await Promise.all([
@@ -203,7 +265,7 @@ export function createPolygonStakingService({
203
265
  })
204
266
  }
205
267
 
206
- async function claimUndelegatedBalance({ walletAccount, unbondNonce, feeData } = {}) {
268
+ async function claimUndelegatedBalance({ walletAccount, unbondNonce, feeData }) {
207
269
  feeData = await resolveOptionalFeeData({ feeData })
208
270
 
209
271
  const [delegatorAddress, { asset, stakingApi }] = await Promise.all([
@@ -301,7 +363,7 @@ export function createPolygonStakingService({
301
363
  * This is just for displaying purposes and it's just an aproximation of the delegate gas cost,
302
364
  * NOT the real fee cost
303
365
  */
304
- async function estimateDelegateTxFee({ feeData } = {}) {
366
+ async function estimateDelegateTxFee({ feeData } = Object.create(null)) {
305
367
  // approx gas limits
306
368
  const { ethereum } = await assetClientInterface.getAssetsForNetwork({
307
369
  baseAssetName: 'ethereum',
@@ -346,7 +408,8 @@ export function createPolygonStakingService({
346
408
  gasPrice: DISABLE_BALANCE_CHECKS,
347
409
  })
348
410
 
349
- return ethereum.api.getFee({ asset: ethereum, feeData, gasLimit, amount })
411
+ const fee = ethereum.api.getFee({ asset: ethereum, feeData, gasLimit, amount })
412
+ return { ...fee, gasLimit }
350
413
  }
351
414
 
352
415
  async function prepareAndSendTx({
@@ -358,7 +421,7 @@ export function createPolygonStakingService({
358
421
  tipGasPrice,
359
422
  waitForConfirmation = false,
360
423
  feeData,
361
- } = {}) {
424
+ }) {
362
425
  const { ethereum: asset } = await assetClientInterface.getAssetsForNetwork({
363
426
  baseAssetName: 'ethereum',
364
427
  })
@@ -4,6 +4,12 @@ import { isEthereumLikeToken, parseUnsignedTx } from '@exodus/ethereum-lib'
4
4
  import { bufferToHex } from '@exodus/ethereumjs/util'
5
5
  import assert from 'minimalistic-assert'
6
6
 
7
+ import { MaticStakingApi } from '../staking/matic/api.js'
8
+ import {
9
+ DELEGATE,
10
+ maticDelegateOptimisticSideEffectTxLogs,
11
+ } from '../staking/matic/matic-staking-utils.js'
12
+
7
13
  // Returns the most competitively priced pending
8
14
  // transaction from the `TxLog` for a given `nonce`.
9
15
  //
@@ -50,6 +56,7 @@ export const getHighestIncentiveTxByNonce = async ({
50
56
  export const getOptimisticTxLogEffects = async ({
51
57
  asset,
52
58
  assetClientInterface,
59
+ bundleId,
53
60
  confirmations = 0,
54
61
  date = SynchronizedTime.now(),
55
62
  fromAddress,
@@ -108,6 +115,23 @@ export const getOptimisticTxLogEffects = async ({
108
115
  console.log('Attempting to replace a transaction using an insufficient fee!')
109
116
  }
110
117
 
118
+ // Contains effects for smart-contract initiated token movements.
119
+ const methodOptimisticSideEffects = await operationTxLogSideEffects({
120
+ txToAddress: to,
121
+ methodId,
122
+ asset,
123
+ walletAccount,
124
+ feeAmount,
125
+ txId,
126
+ nonce,
127
+ gasLimit,
128
+ tipGasPrice: maybeTipGasPrice,
129
+ data,
130
+ date,
131
+ bundleId,
132
+ })
133
+
134
+ // Fallback to basic logic that only handles transfers + fee decoding
111
135
  const sharedProps = {
112
136
  confirmations,
113
137
  date,
@@ -122,6 +146,7 @@ export const getOptimisticTxLogEffects = async ({
122
146
  nonce,
123
147
  ...(maybeTipGasPrice ? { tipGasPrice: maybeTipGasPrice.toBaseString() } : null),
124
148
  ...(methodId ? { methodId } : null),
149
+ ...(bundleId ? { bundleId } : null),
125
150
  },
126
151
  }
127
152
 
@@ -157,7 +182,48 @@ export const getOptimisticTxLogEffects = async ({
157
182
  },
158
183
  ],
159
184
  },
185
+ ...methodOptimisticSideEffects,
160
186
  ].filter(Boolean)
161
187
 
162
188
  return { optimisticTxLogEffects, nonce }
163
189
  }
190
+
191
+ const operationTxLogSideEffects = async ({
192
+ txToAddress,
193
+ methodId,
194
+ asset,
195
+ walletAccount,
196
+ feeAmount,
197
+ txId,
198
+ nonce,
199
+ gasLimit,
200
+ tipGasPrice,
201
+ data,
202
+ date,
203
+ bundleId,
204
+ }) => {
205
+ // Matic Delegate
206
+ if (
207
+ txToAddress?.toLowerCase() ===
208
+ MaticStakingApi.EVERSTAKE_VALIDATOR_CONTRACT_ADDR.toLowerCase() &&
209
+ methodId === DELEGATE
210
+ ) {
211
+ return maticDelegateOptimisticSideEffectTxLogs({
212
+ asset,
213
+ walletAccount,
214
+ feeAmount,
215
+ delegateTxId: txId,
216
+ nonce,
217
+ estimatedDelegateGasLimit: gasLimit,
218
+ tipGasPrice,
219
+ transactionData: data,
220
+ date,
221
+ bundleId,
222
+ })
223
+ }
224
+
225
+ // Add other officially supported operations here
226
+ // Then can fallback to simulation results (TODO)
227
+ // Then finally to the default basic logic
228
+ return []
229
+ }
@@ -1,7 +1,9 @@
1
1
  import { asset as ethereum } from '@exodus/ethereum-meta'
2
2
  import { asset as ethereumholesky } from '@exodus/ethereumholesky-meta'
3
3
 
4
- import { EthereumStaking, isEthereumStakingTx, MaticStakingApi } from '../staking/index.js'
4
+ import { EthereumStaking } from '../staking/ethereum/api.js'
5
+ import { isEthereumStakingTx } from '../staking/ethereum/staking-utils.js'
6
+ import { MaticStakingApi } from '../staking/matic/api.js'
5
7
 
6
8
  const polygonStakingApi = new MaticStakingApi()
7
9
  const ethereumStakingApi = new EthereumStaking(ethereum)