@exodus/ethereum-api 8.76.1 → 8.76.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,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
+ ## [8.76.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.1...@exodus/ethereum-api@8.76.2) (2026-05-27)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix(ethereum-api): persist rawCoinAmount in tx-log monitor readers (#8131)
13
+
14
+
15
+
6
16
  ## [8.76.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.0...@exodus/ethereum-api@8.76.1) (2026-05-26)
7
17
 
8
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.76.1",
3
+ "version": "8.76.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",
@@ -70,5 +70,5 @@
70
70
  "type": "git",
71
71
  "url": "git+https://github.com/ExodusMovement/assets.git"
72
72
  },
73
- "gitHead": "50c5568a3fb1663b61344b18a044ebc69bfd45f0"
73
+ "gitHead": "6c398eaf7bcaecf41b720833f1dae5b8c56b3d57"
74
74
  }
package/src/tx-create.js CHANGED
@@ -257,14 +257,29 @@ const createBumpUnsignedTx = async ({
257
257
  }
258
258
  }
259
259
 
260
- const toAddress = (replacedTokenTx || replacedTx).to
260
+ const sourceTx = replacedTokenTx || replacedTx
261
+ const toAddress = sourceTx.to
262
+
263
+ // For self-sends `coinAmount` is `0` (correct for balance accounting, the
264
+ // wallet's net delta really is zero), so we can't reconstruct the on-chain
265
+ // value from it. Recover from `data.rawCoinAmount`, which the tx-log
266
+ // writers persist alongside `coinAmount` for exactly this purpose.
267
+ if (sourceTx.selfSend && !sourceTx.data?.rawCoinAmount) {
268
+ throw new Error(
269
+ `Cannot bump self-send transaction ${bumpTxId}: original transfer amount is unrecoverable from the tx log (data.rawCoinAmount missing). Re-broadcast the send with a higher fee instead.`
270
+ )
271
+ }
261
272
 
262
- const amount = (replacedTokenTx || replacedTx).coinAmount.negate()
273
+ const amount = sourceTx.data?.rawCoinAmount
274
+ ? asset.currency.baseUnit(sourceTx.data.rawCoinAmount)
275
+ : sourceTx.coinAmount.negate()
263
276
 
264
277
  const txValue = assertValidTxValue({
265
278
  asset,
266
279
  amount,
267
- txValue: replacedTx.coinAmount.negate(),
280
+ txValue: replacedTx.data?.rawCoinAmount
281
+ ? baseAsset.currency.baseUnit(replacedTx.data.rawCoinAmount)
282
+ : replacedTx.coinAmount.negate(),
268
283
  })
269
284
 
270
285
  const isDuplex = Boolean(replacedTokenTx && txValue.gt(baseAsset.currency.ZERO))
@@ -2,6 +2,7 @@ import lodash from 'lodash'
2
2
 
3
3
  import { getWalletUpdates } from '../monitor-utils/get-balance-updates.js'
4
4
  import getFeeAmount from '../monitor-utils/get-fee-amount.js'
5
+ import getRawTransferAmount from '../monitor-utils/get-raw-transfer-amount.js'
5
6
  import getTransfersByTokenName from '../monitor-utils/get-transfers-by-token-name.js'
6
7
  import getValueOfTransfers from '../monitor-utils/get-value-of-transfers.js'
7
8
  import isConfirmedServerTx from '../monitor-utils/is-confirmed-server-tx.js'
@@ -62,6 +63,18 @@ export default function getLogItemsFromServerTx({
62
63
 
63
64
  if (shouldAttachTx) {
64
65
  const coinAmount = getValueOfTransfers(ourWalletAddress, asset, ethereumTransfers)
66
+ // `rawCoinAmount` mirrors the field written by `getOptimisticTxLogEffects`
67
+ // and is consumed by the bump path (`tx-create.js`) to recover the
68
+ // user-signed on-chain `value` when accelerating a self-send (where
69
+ // `coinAmount` is zero). For the base-asset entry the raw amount is the
70
+ // top-level `value` field of the tx itself, not the sum of internal
71
+ // transfers — that's what the user signed and what we must restore on
72
+ // bump.
73
+ const rawCoinAmount = getRawTransferAmount({
74
+ ourWalletAddress,
75
+ asset,
76
+ transfers: [serverTx],
77
+ }).toBaseString()
65
78
  const selfSend = isSelfSendTx({
66
79
  coinAmount,
67
80
  ourWalletWasSender,
@@ -90,6 +103,7 @@ export default function getLogItemsFromServerTx({
90
103
  data,
91
104
  nonce,
92
105
  gasLimit,
106
+ rawCoinAmount,
93
107
  balanceChange: baseBalanceUpdate,
94
108
  nonceChange: nonceUpdate,
95
109
  ...methodId,
@@ -130,6 +144,14 @@ export default function getLogItemsFromServerTx({
130
144
 
131
145
  const tokenTransferToAddress = tryFindExternalRecipient(tokenTransfers, ourWalletAddress)
132
146
  const coinAmount = getValueOfTransfers(ourWalletAddress, token, tokenTransfers)
147
+ // See base-entry comment above; for token entries the raw amount is the
148
+ // absolute total tokens our wallet moved in this tx (sum of outgoing, or
149
+ // incoming for receive-only entries).
150
+ const rawCoinAmount = getRawTransferAmount({
151
+ ourWalletAddress,
152
+ asset: token,
153
+ transfers: tokenTransfers,
154
+ }).toBaseString()
133
155
 
134
156
  const tokenFromAddresses = lodash.uniq(
135
157
  tokenTransfers.filter(({ to }) => to === ourWalletAddress).map(({ from }) => from)
@@ -158,6 +180,7 @@ export default function getLogItemsFromServerTx({
158
180
  data,
159
181
  nonce,
160
182
  gasLimit,
183
+ rawCoinAmount,
161
184
  balanceChange,
162
185
  ...methodId,
163
186
  ...(isFiniteInteger(blockNumber) ? { blockNumber } : null),
@@ -158,6 +158,7 @@ export const getOptimisticTxLogEffects = async ({
158
158
  txs: [
159
159
  {
160
160
  ...sharedProps,
161
+ data: { ...sharedProps.data, rawCoinAmount: amount.abs().toBaseString() },
161
162
  coinAmount: selfSend ? asset.currency.ZERO : amount.abs().negate(),
162
163
  coinName: asset.name,
163
164
  currencies: {
@@ -173,6 +174,7 @@ export const getOptimisticTxLogEffects = async ({
173
174
  txs: [
174
175
  {
175
176
  ...sharedProps,
177
+ data: { ...sharedProps.data, rawCoinAmount: txValue.abs().toBaseString() },
176
178
  coinAmount: selfSend ? baseAsset.currency.ZERO : txValue.abs().negate(),
177
179
  coinName: baseAsset.name,
178
180
  currencies: {
@@ -2,6 +2,7 @@ import lodash from 'lodash'
2
2
 
3
3
  import getFeeAmount from './get-fee-amount.js'
4
4
  import getNamesOfTokensTransferredByServerTx from './get-names-of-tokens-transferred-by-server-tx.js'
5
+ import getRawTransferAmount from './get-raw-transfer-amount.js'
5
6
  import getTransfersByTokenName from './get-transfers-by-token-name.js'
6
7
  import getValueOfTransfers from './get-value-of-transfers.js'
7
8
  import isConfirmedServerTx from './is-confirmed-server-tx.js'
@@ -50,6 +51,18 @@ export default function getLogItemsFromServerTx({
50
51
 
51
52
  if (sendingTransferPresent || receivingTransferPresent || nftTransferPresent) {
52
53
  const coinAmount = getValueOfTransfers(ourWalletAddress, asset, ethereumTransfers)
54
+ // `rawCoinAmount` mirrors the field written by `getOptimisticTxLogEffects`
55
+ // and is consumed by the bump path (`tx-create.js`) to recover the
56
+ // user-signed on-chain `value` when accelerating a self-send (where
57
+ // `coinAmount` is zero). For the base-asset entry the raw amount is the
58
+ // top-level `value` field of the tx itself, not the sum of internal
59
+ // transfers — that's what the user signed and what we must restore on
60
+ // bump.
61
+ const rawCoinAmount = getRawTransferAmount({
62
+ ourWalletAddress,
63
+ asset,
64
+ transfers: [serverTx],
65
+ }).toBaseString()
53
66
  const selfSend = isSelfSendTx({
54
67
  coinAmount,
55
68
  ourWalletWasSender,
@@ -78,6 +91,7 @@ export default function getLogItemsFromServerTx({
78
91
  data,
79
92
  nonce,
80
93
  gasLimit,
94
+ rawCoinAmount,
81
95
  ...methodId,
82
96
  ...(sent?.length > 0 ? { sent } : undefined),
83
97
  },
@@ -108,11 +122,25 @@ export default function getLogItemsFromServerTx({
108
122
 
109
123
  const token = assets[tokenName]
110
124
  const tokenTransferToAddress = tryFindExternalRecipient(tokenTransfers, ourWalletAddress)
111
- const coinAmount = getValueOfTransfers(
125
+ // Transfers from this reader's server source are tagged with
126
+ // `events: true` once the tx is included in a block (event-log derived)
127
+ // and `events: false` while still pending (tx-data derived). Only one
128
+ // flavor is real for a given lifecycle state; filtering avoids
129
+ // double-counting if both ever coexist for the same transfer. Both
130
+ // `coinAmount` and `rawCoinAmount` must see the same set so they stay
131
+ // consistent.
132
+ const filteredTokenTransfers = lodash.filter(tokenTransfers, {
133
+ events: confirmations > 0,
134
+ })
135
+ const coinAmount = getValueOfTransfers(ourWalletAddress, token, filteredTokenTransfers)
136
+ // See base-entry comment above; for token entries the raw amount is the
137
+ // absolute total tokens our wallet moved in this tx (sum of outgoing, or
138
+ // incoming for receive-only entries).
139
+ const rawCoinAmount = getRawTransferAmount({
112
140
  ourWalletAddress,
113
- token,
114
- lodash.filter(tokenTransfers, { events: confirmations > 0 })
115
- )
141
+ asset: token,
142
+ transfers: filteredTokenTransfers,
143
+ }).toBaseString()
116
144
  const tokenFromAddresses = lodash.uniq(
117
145
  tokenTransfers.filter(({ to }) => to === ourWalletAddress).map(({ from }) => from)
118
146
  )
@@ -136,7 +164,7 @@ export default function getLogItemsFromServerTx({
136
164
  ...logItemCommonProperties,
137
165
  coinAmount,
138
166
  coinName: tokenName,
139
- data: { data, nonce, gasLimit, ...methodId },
167
+ data: { data, nonce, gasLimit, rawCoinAmount, ...methodId },
140
168
  ...(isConsideredSent
141
169
  ? { from: [], to: tokenTransferToAddress, feeAmount, feeCoinName: asset.feeAsset.name }
142
170
  : { from: tokenFromAddresses }),
@@ -0,0 +1,29 @@
1
+ // Returns the absolute raw transferred amount for `ourWalletAddress`, in
2
+ // `asset` base units, as a NumberUnit. Unlike `getValueOfTransfers` (which
3
+ // returns the signed net delta), this preserves the raw transfer value even
4
+ // when sender and receiver net to zero — e.g. self-sends. Used by tx-log
5
+ // writers to persist `data.rawCoinAmount` so the bump path can reconstruct
6
+ // the original on-chain value when accelerating a self-send.
7
+ //
8
+ // Each transfer is expected to have the shape `{ from, to, value }`. For the
9
+ // base-asset entry, pass `[serverTx]` (the tx itself has the same shape at
10
+ // its top level); for token entries, pass the array of token transfers for
11
+ // that token.
12
+ //
13
+ // Outgoing transfers (from === ourWalletAddress) take precedence: when our
14
+ // wallet sent anything, the raw amount is the sum of what we sent. We fall
15
+ // back to incoming so receiver-only entries still report a non-zero raw
16
+ // amount for consistency, even though the bump path only consults it on the
17
+ // sender side.
18
+
19
+ export default function getRawTransferAmount({ ourWalletAddress, asset, transfers }) {
20
+ const outgoing = transfers
21
+ .filter(({ from }) => from === ourWalletAddress)
22
+ .reduce((sum, { value }) => sum.add(asset.currency.baseUnit(value)), asset.currency.ZERO)
23
+
24
+ if (!outgoing.isZero) return outgoing
25
+
26
+ return transfers
27
+ .filter(({ to }) => to === ourWalletAddress)
28
+ .reduce((sum, { value }) => sum.add(asset.currency.baseUnit(value)), asset.currency.ZERO)
29
+ }