@exodus/ethereum-api 8.78.0 → 8.78.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,45 @@
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.78.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.77.1...@exodus/ethereum-api@8.78.2) (2026-08-14)
7
+
8
+
9
+ ### Features
10
+
11
+ * source eip7702 delegation whitelist from remote config ([#8356](https://github.com/ExodusMovement/assets/issues/8356)) ([1a0966b](https://github.com/ExodusMovement/assets/commit/1a0966b08befb6e9310993f323c21d003dea2754))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **ethereum-api:** detect legacy delegate transactions ([#8357](https://github.com/ExodusMovement/assets/issues/8357)) ([070e529](https://github.com/ExodusMovement/assets/commit/070e5295f21f12695e97e721ff8c0601c553e447))
17
+ * **ethereum-api:** preserve precision for wei values in tx simulation ([#7920](https://github.com/ExodusMovement/assets/issues/7920)) ([5c99258](https://github.com/ExodusMovement/assets/commit/5c9925871d450167fcf23e33ba62e100a2d71a9b))
18
+ * **ethereum-api:** reference defined ethereum asset in simulate-tx tests ([#8525](https://github.com/ExodusMovement/assets/issues/8525)) ([27a8918](https://github.com/ExodusMovement/assets/commit/27a8918f5ba3112f1830ac0111a43d30921aa3b3))
19
+ * **ethereum:** prevent silently dropped zero-tip private bundles ([#8493](https://github.com/ExodusMovement/assets/issues/8493)) ([67dd1ff](https://github.com/ExodusMovement/assets/commit/67dd1ffd7e1c22e7119b1781968c4be321ab1822))
20
+ * **lint:** fix import/named warnings and upgrade to error ([#8496](https://github.com/ExodusMovement/assets/issues/8496)) ([8d290c9](https://github.com/ExodusMovement/assets/commit/8d290c948a0d3c2c1897287cb133f9d26cad73b0))
21
+ * **lint:** fix JSDoc warnings across multiple packages ([#8519](https://github.com/ExodusMovement/assets/issues/8519)) ([5fefcd5](https://github.com/ExodusMovement/assets/commit/5fefcd5af996efbe9587cecba540efc7f469ceb2))
22
+ * **lint:** fix no-throw-literal warnings and upgrade to error ([#8498](https://github.com/ExodusMovement/assets/issues/8498)) ([8fc68eb](https://github.com/ExodusMovement/assets/commit/8fc68eb65682e0b5a7da9415410ee1dedf309696))
23
+ * **lint:** fix oxlint warnings and convert rules to error ([#8489](https://github.com/ExodusMovement/assets/issues/8489)) ([19d35be](https://github.com/ExodusMovement/assets/commit/19d35be718391303d331978926884f2f376b356f))
24
+ * **simulation:** route recipient changes to dedicated field, guard asset-less entries ([#8370](https://github.com/ExodusMovement/assets/issues/8370)) ([c0a082b](https://github.com/ExodusMovement/assets/commit/c0a082b6c80710408337ff9285a19fb657c656be))
25
+ * upgrade low-complexity lint rules to error ([#8510](https://github.com/ExodusMovement/assets/issues/8510)) ([25016bd](https://github.com/ExodusMovement/assets/commit/25016bdf43526c6bc9efe2771f92f1ae1ebf7cd7))
26
+
27
+
28
+
29
+ ## [8.78.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.77.1...@exodus/ethereum-api@8.78.1) (2026-08-13)
30
+
31
+
32
+ ### Features
33
+
34
+ * source eip7702 delegation whitelist from remote config ([#8356](https://github.com/ExodusMovement/assets/issues/8356)) ([1a0966b](https://github.com/ExodusMovement/assets/commit/1a0966b08befb6e9310993f323c21d003dea2754))
35
+
36
+
37
+ ### Bug Fixes
38
+
39
+ * **ethereum-api:** detect legacy delegate transactions ([#8357](https://github.com/ExodusMovement/assets/issues/8357)) ([070e529](https://github.com/ExodusMovement/assets/commit/070e5295f21f12695e97e721ff8c0601c553e447))
40
+ * **lint:** fix oxlint warnings and convert rules to error ([#8489](https://github.com/ExodusMovement/assets/issues/8489)) ([19d35be](https://github.com/ExodusMovement/assets/commit/19d35be718391303d331978926884f2f376b356f))
41
+ * **simulation:** route recipient changes to dedicated field, guard asset-less entries ([#8370](https://github.com/ExodusMovement/assets/issues/8370)) ([c0a082b](https://github.com/ExodusMovement/assets/commit/c0a082b6c80710408337ff9285a19fb657c656be))
42
+
43
+
44
+
6
45
  ## [8.78.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.77.1...@exodus/ethereum-api@8.78.0) (2026-08-04)
7
46
 
8
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.78.0",
3
+ "version": "8.78.2",
4
4
  "description": "Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Ethereum and EVM-based blockchains",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -17,9 +17,7 @@
17
17
  "provenance": false
18
18
  },
19
19
  "scripts": {
20
- "test": "run -T exodus-test --jest",
21
- "lint": "run -T eslintc .",
22
- "lint:fix": "yarn lint --fix"
20
+ "test": "run -T exodus-test --jest"
23
21
  },
24
22
  "dependencies": {
25
23
  "@exodus/asset": "^2.0.4",
@@ -69,5 +67,5 @@
69
67
  "type": "git",
70
68
  "url": "git+https://github.com/ExodusMovement/assets.git"
71
69
  },
72
- "gitHead": "e878130c84c596ee86050984806b0da491e14910"
70
+ "gitHead": "07b29ff2dc71fb1358ad6b1b6748c3f9fe66c10e"
73
71
  }
@@ -1,4 +1,5 @@
1
1
  import { BlacklistCheckTypes } from '@exodus/asset-lib'
2
+ import { parseUnsignedTx } from '@exodus/ethereum-lib'
2
3
  import { defaultAbiCoder } from '@exodus/ethereumjs/ethers5-abi'
3
4
  import { safeString } from '@exodus/safe-string'
4
5
  import assert from 'minimalistic-assert'
@@ -112,6 +113,29 @@ const assertSendPrivateTxProps = ({ asset, unsignedTx, walletAccount }) => {
112
113
  assert(walletAccount, 'expected walletAccount')
113
114
  }
114
115
 
116
+ // Block builders drop private transactions that carry no bid — accepted by
117
+ // the relay but never included on chain (ExodusMovement/assets#8084). The
118
+ // effective bid is min(tipGasPrice, gasPrice - baseFeePerGas): `gasPrice` is
119
+ // the maxFeePerGas ceiling for EIP-1559 transactions and the all-in price
120
+ // for legacy ones, so either term at zero kills the bid.
121
+ const assertNonZeroTipGasPrice = async ({ asset, assetClientInterface, unsignedTx }) => {
122
+ const { eip1559Enabled, gasPrice, tipGasPrice } = parseUnsignedTx({ asset, unsignedTx })
123
+
124
+ if (eip1559Enabled && (!tipGasPrice || tipGasPrice.isZero)) {
125
+ throw new Error(
126
+ 'ERR_PRIVATE_TX_ZERO_TIP: transaction pays no tipGasPrice; block builders drop zero-bid private transactions'
127
+ )
128
+ }
129
+
130
+ const feeData = await assetClientInterface.getFeeConfig({ assetName: asset.baseAsset.name })
131
+ const baseFeePerGas = feeData?.baseFeePerGas
132
+ if (!baseFeePerGas || gasPrice.gt(baseFeePerGas)) return
133
+
134
+ throw new Error(
135
+ `ERR_PRIVATE_TX_ZERO_TIP: transaction gasPrice (${gasPrice.toBaseString()}) does not exceed baseFeePerGas (${baseFeePerGas.toBaseString()}); block builders drop zero-bid private transactions`
136
+ )
137
+ }
138
+
115
139
  export const createTransactionPrivacyResult = ({
116
140
  assetClientInterface,
117
141
  broadcastPrivateBundle,
@@ -123,6 +147,7 @@ export const createTransactionPrivacyResult = ({
123
147
 
124
148
  const sendPrivateTx = async ({ asset, unsignedTx, walletAccount }) => {
125
149
  assertSendPrivateTxProps({ asset, unsignedTx, walletAccount })
150
+ await assertNonZeroTipGasPrice({ asset, assetClientInterface, unsignedTx })
126
151
 
127
152
  const { rawTx, txId } = await signTx({
128
153
  asset,
@@ -160,6 +185,10 @@ export const createTransactionPrivacyResult = ({
160
185
 
161
186
  unsignedTxBundle.forEach(assertSendPrivateTxProps)
162
187
 
188
+ for (const { asset, unsignedTx } of unsignedTxBundle) {
189
+ await assertNonZeroTipGasPrice({ asset, assetClientInterface, unsignedTx })
190
+ }
191
+
163
192
  const signTxResults = await Promise.all(
164
193
  unsignedTxBundle.map(({ asset, unsignedTx, walletAccount }) =>
165
194
  signTx({ assetClientInterface, asset, unsignedTx, walletAccount })
@@ -21,8 +21,8 @@ const parseSubscriptionKey = (subscriptionKey) => {
21
21
 
22
22
  /**
23
23
  * @typedef subscribeWalletAddresses
24
- * @property {string} network
25
- * @property {string} address
24
+ * @property {string} network - The network identifier
25
+ * @property {string} address - The wallet address to subscribe
26
26
  */
27
27
 
28
28
  export default class WsGateway extends EventEmitter {
package/src/fee-utils.js CHANGED
@@ -111,9 +111,18 @@ export const calculateEthLikeFeeMonitorUpdateEip1559 = async ({
111
111
  // at least the `baseFeePerGas`.
112
112
  const gasPrice = baseFeePerGas > defaultGasPrice ? baseFeePerGas : defaultGasPrice
113
113
 
114
+ // `eth_gasPrice` embeds the node's prevailing tip on top of the
115
+ // `baseFeePerGas`. Without an explicit `tipGasPrice`, `refineFeeConfig`
116
+ // collapses the effective gas price to the bare base fee, producing
117
+ // zero-tip transactions that private bundle relays drop silently.
118
+ const inferredTipGasPrice = gasPrice - baseFeePerGas
119
+
114
120
  const defaultFeeConfig = {
115
121
  gasPrice: `${gasPrice.toString()} wei`,
116
122
  baseFeePerGas: `${baseFeePerGas.toString()} wei`,
123
+ ...(inferredTipGasPrice > BigInt(0) && {
124
+ tipGasPrice: `${inferredTipGasPrice.toString()} wei`,
125
+ }),
117
126
  }
118
127
 
119
128
  // Determine if there are any `rewardPercentiles` specified in the
package/src/index.js CHANGED
@@ -41,28 +41,29 @@ export { getStakingHistoryBalance, getBalancesFactory } from './get-balances.js'
41
41
  export { getHistoricalBalanceFactory } from './get-historical-balance.js'
42
42
  export { createEncodeMultisigContract } from './multisig-address.js'
43
43
 
44
+ export { FantomStaking, stakingProviderClientFactory } from './staking/index.js'
45
+ export { ethereumStakingDeps } from './staking/ethereum/deps.js'
46
+ export { getEthereumStakingInfo, createEthereumStakingService } from './staking/ethereum/service.js'
47
+ export { EthereumStaking } from './staking/ethereum/api.js'
44
48
  export {
45
- FantomStaking,
46
- stakingProviderClientFactory,
47
- ethereumStakingDeps,
48
- getEthereumStakingInfo,
49
- createEthereumStakingService,
50
- EthereumStaking,
51
49
  isEthereumStakingTx,
52
50
  isEthereumDelegate,
53
51
  isEthereumUndelegatePending,
54
52
  isEthereumUndelegate,
55
53
  isEthereumClaimUndelegate,
56
- MaticStakingApi,
54
+ } from './staking/ethereum/staking-utils.js'
55
+ export { MaticStakingApi } from './staking/matic/index.js'
56
+ export {
57
57
  isPolygonTx,
58
58
  isPolygonDelegate,
59
59
  isPolygonUndelegate,
60
60
  isPolygonReward,
61
61
  isPolygonClaimUndelegate,
62
62
  isPendingPolygonUndelegateTxInEthereumTxLog,
63
- } from './staking/index.js'
63
+ } from './staking/matic/matic-staking-utils.js'
64
64
 
65
- export { fetchTxPreview, maybeRemoveDuplicates, retrieveSideEffects } from './simulate-tx/index.js'
65
+ export { fetchTxPreview } from './simulate-tx/fetch-tx-preview.js'
66
+ export { maybeRemoveDuplicates, retrieveSideEffects } from './simulate-tx/simulate-eth-tx.js'
66
67
  export { simulateTransactions } from './simulation/simulate-transactions.js'
67
68
 
68
69
  export {
@@ -8,11 +8,22 @@ import SolidityContract from '@exodus/solidity-contract'
8
8
  import assert from 'minimalistic-assert'
9
9
 
10
10
  import { getERC20Params } from '../eth-like-util.js'
11
+ import { fromHexToBigInt } from '../number-utils.js'
11
12
  import { fetchTxPreview } from './fetch-tx-preview.js'
12
13
 
13
14
  const ethDecimals = ethereum.units.ETH
14
15
 
15
- const ethHexToInt = (hexValue) => parseInt(hexValue, '16')
16
+ // Convert a hex transaction field (e.g. "0x10a741a462780000" for 1.2 ETH in wei)
17
+ // to a precision-preserving decimal string. `parseInt` would silently truncate
18
+ // values above Number.MAX_SAFE_INTEGER (~9e15), and Ethereum wei amounts routinely
19
+ // exceed that (1 ETH = 1e18 wei).
20
+ const ethHexToDecimalString = (hexValue) => {
21
+ if (hexValue === undefined || hexValue === null || hexValue === '' || hexValue === '0x') {
22
+ return '0'
23
+ }
24
+
25
+ return fromHexToBigInt(hexValue).toString()
26
+ }
16
27
 
17
28
  async function getAssetSymbolFromContract(contractAddress, asset) {
18
29
  const { symbol: assetSymbol } = await getERC20Params({
@@ -169,14 +180,16 @@ export async function retrieveSideEffects({ transaction, asset, shouldSimulate =
169
180
 
170
181
  if (!transaction.to) throw new Error(`'to' field is missing in the TX object`)
171
182
 
183
+ const txValueDecimal = ethHexToDecimalString(transaction.value)
184
+
172
185
  const blocknativeTxObject = {
173
186
  to: transaction.to,
174
187
  input: transaction.data,
175
188
  from: transaction.from,
176
- value: ethHexToInt(transaction.value),
177
- gas: ethHexToInt(transaction.gas),
178
- maxFeePerGas: ethHexToInt(transaction.maxFeePerGas),
179
- maxPriorityFeePerGas: ethHexToInt(transaction.maxPriorityFeePerGas),
189
+ value: txValueDecimal,
190
+ gas: ethHexToDecimalString(transaction.gas),
191
+ maxFeePerGas: ethHexToDecimalString(transaction.maxFeePerGas),
192
+ maxPriorityFeePerGas: ethHexToDecimalString(transaction.maxPriorityFeePerGas),
180
193
  }
181
194
 
182
195
  try {
@@ -224,7 +237,7 @@ export async function retrieveSideEffects({ transaction, asset, shouldSimulate =
224
237
  if (willSend.length === 0) {
225
238
  willSend.push({
226
239
  symbol: 'ETH',
227
- balance: `-${ethHexToInt(transaction.value)}`,
240
+ balance: `-${txValueDecimal}`,
228
241
  assetType: 'ether',
229
242
  decimal: ethDecimals,
230
243
  })
@@ -284,7 +284,9 @@ export class ClarityMonitor extends BaseMonitor {
284
284
  }
285
285
 
286
286
  if (historyError) {
287
- throw historyError
287
+ throw historyError instanceof Error
288
+ ? historyError
289
+ : new Error(String(historyError), { cause: historyError })
288
290
  }
289
291
 
290
292
  if (refresh || hasNewTxs) {
@@ -229,7 +229,9 @@ export class ClarityTruncatedHistoryMonitor extends ClarityMonitor {
229
229
  }
230
230
 
231
231
  if (historyError) {
232
- throw historyError
232
+ throw historyError instanceof Error
233
+ ? historyError
234
+ : new Error(String(historyError), { cause: historyError })
233
235
  }
234
236
 
235
237
  if (refresh || hasNewTxs) {
@@ -20,8 +20,8 @@ const isCanonicalAbsoluteTx = (tx, fieldName) => {
20
20
 
21
21
  return Boolean(
22
22
  tx?.data?.[fieldName] &&
23
- typeof tx.data.blockNumber === 'number' &&
24
- typeof tx.data.transactionIndex === 'number'
23
+ typeof tx.data.blockNumber === 'number' &&
24
+ typeof tx.data.transactionIndex === 'number'
25
25
  )
26
26
  }
27
27
 
@@ -14,6 +14,24 @@ const isFiniteInteger = (n) => Number.isFinite(n) && Number.isInteger(n)
14
14
 
15
15
  // This function takes a server transaction object fetched from clarity,
16
16
  // and transforms it into Tx models to update the exodus state.
17
+
18
+ const tryFindExternalRecipient = (transfers, ourWalletAddress) =>
19
+ transfers.find(({ to }) => to !== ourWalletAddress)?.to || ourWalletAddress
20
+
21
+ // If the timestamp is in the future, use the current time.
22
+ const parseServerTxDate = (timestamp) => new Date(Math.min(Date.now(), parseInt(timestamp, 16)))
23
+
24
+ function isSelfSendTx({
25
+ coinAmount,
26
+ ourWalletWasSender,
27
+ sendingTransferPresent,
28
+ receivingTransferPresent,
29
+ }) {
30
+ return (
31
+ coinAmount.isZero && sendingTransferPresent && receivingTransferPresent && ourWalletWasSender
32
+ )
33
+ }
34
+
17
35
  export default function getLogItemsFromServerTx({
18
36
  serverTx,
19
37
  asset,
@@ -200,20 +218,3 @@ export default function getLogItemsFromServerTx({
200
218
 
201
219
  return Object.fromEntries(logItemsForServerTxEntries)
202
220
  }
203
-
204
- const tryFindExternalRecipient = (transfers, ourWalletAddress) =>
205
- transfers.find(({ to }) => to !== ourWalletAddress)?.to || ourWalletAddress
206
-
207
- // If the timestamp is in the future, use the current time.
208
- const parseServerTxDate = (timestamp) => new Date(Math.min(Date.now(), parseInt(timestamp, 16)))
209
-
210
- function isSelfSendTx({
211
- coinAmount,
212
- ourWalletWasSender,
213
- sendingTransferPresent,
214
- receivingTransferPresent,
215
- }) {
216
- return (
217
- coinAmount.isZero && sendingTransferPresent && receivingTransferPresent && ourWalletWasSender
218
- )
219
- }
@@ -167,7 +167,7 @@ export class EthereumMonitor extends BaseMonitor {
167
167
  server: this.server,
168
168
  ourWalletAddress: derivedData.ourWalletAddress,
169
169
  minimumConfirmations: derivedData.minimumConfirmations,
170
- index: refresh ? 0 : derivedIndex ?? 0,
170
+ index: refresh ? 0 : (derivedIndex ?? 0),
171
171
  })
172
172
  const hasNewIndex = !derivedIndex || index > derivedIndex
173
173
 
@@ -11,6 +11,28 @@ import isConsideredSentTokenTx from './is-considered-sent-token-tx.js'
11
11
  // This function takes a server transaction object fetched from magnifier,
12
12
  // and transforms it into Tx models to update the exodus state.
13
13
 
14
+ const tryFindExternalRecipient = (transfers, ourWalletAddress) =>
15
+ transfers.find(({ to }) => to !== ourWalletAddress)?.to || ourWalletAddress
16
+
17
+ function isSelfSendTx({
18
+ coinAmount,
19
+ ourWalletWasSender,
20
+ sendingTransferPresent,
21
+ receivingTransferPresent,
22
+ }) {
23
+ return (
24
+ coinAmount.isZero && sendingTransferPresent && receivingTransferPresent && ourWalletWasSender
25
+ )
26
+ }
27
+
28
+ function isNftTransfer({ serverTx, ourWalletAddress }) {
29
+ if (!Array.isArray(serverTx.erc721) || serverTx.erc721.length === 0) return false
30
+
31
+ return serverTx.erc721.some(
32
+ (transfer) => transfer.to === ourWalletAddress || transfer.from === ourWalletAddress
33
+ )
34
+ }
35
+
14
36
  export default function getLogItemsFromServerTx({
15
37
  serverTx,
16
38
  asset,
@@ -179,25 +201,3 @@ export default function getLogItemsFromServerTx({
179
201
 
180
202
  return Object.fromEntries(logItemsForServerTxEntries)
181
203
  }
182
-
183
- const tryFindExternalRecipient = (transfers, ourWalletAddress) =>
184
- transfers.find(({ to }) => to !== ourWalletAddress)?.to || ourWalletAddress
185
-
186
- function isSelfSendTx({
187
- coinAmount,
188
- ourWalletWasSender,
189
- sendingTransferPresent,
190
- receivingTransferPresent,
191
- }) {
192
- return (
193
- coinAmount.isZero && sendingTransferPresent && receivingTransferPresent && ourWalletWasSender
194
- )
195
- }
196
-
197
- function isNftTransfer({ serverTx, ourWalletAddress }) {
198
- if (!Array.isArray(serverTx.erc721) || serverTx.erc721.length === 0) return false
199
-
200
- return serverTx.erc721.some(
201
- (transfer) => transfer.to === ourWalletAddress || transfer.from === ourWalletAddress
202
- )
203
- }