@exodus/ethereum-api 8.76.0 → 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 +20 -0
- package/package.json +2 -2
- package/src/error-wrapper.js +8 -2
- package/src/tx-create.js +18 -3
- package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +23 -0
- package/src/tx-log/get-optimistic-txlog-effects.js +2 -0
- package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +33 -5
- package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
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
|
+
|
|
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)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* fix(ethereum-api): classify Cosmos-EVM AnteHandler nonce errors as nonceTooLow (#8130)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
## [8.76.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.75.1...@exodus/ethereum-api@8.76.0) (2026-05-25)
|
|
7
27
|
|
|
8
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.76.
|
|
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": "
|
|
73
|
+
"gitHead": "6c398eaf7bcaecf41b720833f1dae5b8c56b3d57"
|
|
74
74
|
}
|
package/src/error-wrapper.js
CHANGED
|
@@ -146,9 +146,15 @@ const EVM_NORMALIZATION_PATTERNS = [
|
|
|
146
146
|
pattern: /(insufficient funds for gas \* price \+ value|insufficient funds(?!.*balance))/i,
|
|
147
147
|
errorInfo: EVM_ERROR_REASONS.insufficientFundsForGas,
|
|
148
148
|
},
|
|
149
|
-
//
|
|
149
|
+
// Nonce already used. Two flavors:
|
|
150
|
+
// - geth: "nonce too low" / "nonce is too low"
|
|
151
|
+
// - Cosmos-EVM / Ethermint AnteHandler (e.g. Cronos):
|
|
152
|
+
// "invalid nonce; got X, expected Y: invalid sequence: invalid sequence"
|
|
153
|
+
// surfaces from load-balancer node drift (chain ahead of read node).
|
|
154
|
+
// The retry path in tx-send.js bumps to max(nodeNonce, triedNonce + 1)
|
|
155
|
+
// and re-broadcasts. See exodus-desktop#19535.
|
|
150
156
|
{
|
|
151
|
-
pattern:
|
|
157
|
+
pattern: /(\bnonce\b.*\btoo low\b|invalid nonce.*invalid sequence)/i,
|
|
152
158
|
errorInfo: EVM_ERROR_REASONS.nonceTooLow,
|
|
153
159
|
},
|
|
154
160
|
// Comprehensive insufficient balance / transfer exceeds balance pattern (TOKEN balance issues)
|
package/src/tx-create.js
CHANGED
|
@@ -257,14 +257,29 @@ const createBumpUnsignedTx = async ({
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
const
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|