@exodus/ethereum-api 8.50.0 → 8.50.1
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 +8 -0
- package/package.json +2 -2
- package/src/tx-create.js +21 -2
- package/src/tx-log/get-optimistic-txlog-effects.js +130 -0
- package/src/tx-log/index.js +4 -0
- package/src/tx-send/tx-send.js +18 -67
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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.50.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.50.0...@exodus/ethereum-api@8.50.1) (2025-09-22)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @exodus/ethereum-api
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
## [8.50.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.49.0...@exodus/ethereum-api@8.50.0) (2025-09-17)
|
|
7
15
|
|
|
8
16
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.50.
|
|
3
|
+
"version": "8.50.1",
|
|
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",
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"type": "git",
|
|
67
67
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "e54f136a2e7ff694cd12dc1a7b0ef34eb707ef0e"
|
|
70
70
|
}
|
package/src/tx-create.js
CHANGED
|
@@ -8,6 +8,7 @@ import { ensureSaneEip1559GasPriceForTipGasPrice } from './fee-utils.js'
|
|
|
8
8
|
import { ARBITRARY_ADDRESS, fetchGasLimit, resolveDefaultTxInput } from './gas-estimation.js'
|
|
9
9
|
import { getExtraFeeData, getFeeFactoryGasPrices } from './get-fee.js'
|
|
10
10
|
import { getNftArguments } from './nft-utils.js'
|
|
11
|
+
import { getHighestIncentivePendingTxByNonce } from './tx-log/index.js'
|
|
11
12
|
|
|
12
13
|
async function createUnsignedTxWithFees({
|
|
13
14
|
asset,
|
|
@@ -134,6 +135,7 @@ const createBumpUnsignedTx = async ({
|
|
|
134
135
|
const txToAddress = isToken ? asset.contract.address : toAddress
|
|
135
136
|
const coinAmount = (replacedTokenTx || replacedTx).coinAmount.negate()
|
|
136
137
|
const gasLimit = replacedTx.data.gasLimit
|
|
138
|
+
const nonce = replacedTx.data.nonce
|
|
137
139
|
|
|
138
140
|
const value = isToken ? baseAsset.currency.ZERO : coinAmount
|
|
139
141
|
|
|
@@ -143,9 +145,27 @@ const createBumpUnsignedTx = async ({
|
|
|
143
145
|
eip1559Enabled,
|
|
144
146
|
tipGasPrice: currentTipGasPrice,
|
|
145
147
|
} = feeData
|
|
148
|
+
|
|
149
|
+
const maybeHighestIncentivePendingTxForNonce = await getHighestIncentivePendingTxByNonce({
|
|
150
|
+
assetClientInterface,
|
|
151
|
+
asset,
|
|
152
|
+
nonce,
|
|
153
|
+
walletAccount,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
assert(
|
|
157
|
+
maybeHighestIncentivePendingTxForNonce,
|
|
158
|
+
`unable to resolve pending transaction for nonce ${nonce}`
|
|
159
|
+
)
|
|
160
|
+
|
|
146
161
|
const { bumpedGasPrice, bumpedTipGasPrice } = calculateBumpedGasPrice({
|
|
147
162
|
baseAsset,
|
|
148
|
-
|
|
163
|
+
// HACK: Although the `bumpTxId` defines the characteristics of
|
|
164
|
+
// of a transaction the user would like to accelerate, for
|
|
165
|
+
// acceleration to be successful, the transaction must be
|
|
166
|
+
// priced to exceed the miner incentive of whichever
|
|
167
|
+
// transaction is currently pending at the specified nonce.
|
|
168
|
+
tx: maybeHighestIncentivePendingTxForNonce,
|
|
149
169
|
currentGasPrice,
|
|
150
170
|
currentBaseFee,
|
|
151
171
|
currentTipGasPrice,
|
|
@@ -153,7 +173,6 @@ const createBumpUnsignedTx = async ({
|
|
|
153
173
|
})
|
|
154
174
|
const gasPrice = bumpedGasPrice
|
|
155
175
|
const tipGasPrice = bumpedTipGasPrice
|
|
156
|
-
const nonce = replacedTx.data.nonce
|
|
157
176
|
const data = replacedTokenTx
|
|
158
177
|
? asset.contract.transfer.build(toAddress.toLowerCase(), coinAmount.toBaseString())
|
|
159
178
|
: replacedTx.data.data || '0x'
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { isNumberUnit } from '@exodus/currency'
|
|
2
|
+
import { isEthereumLikeToken } from '@exodus/ethereum-lib'
|
|
3
|
+
import assert from 'minimalistic-assert'
|
|
4
|
+
|
|
5
|
+
// Returns the most competitively priced pending
|
|
6
|
+
// transaction from the `TxLog` for a given `nonce`.
|
|
7
|
+
export const getHighestIncentivePendingTxByNonce = async ({
|
|
8
|
+
assetClientInterface,
|
|
9
|
+
asset,
|
|
10
|
+
nonce,
|
|
11
|
+
walletAccount,
|
|
12
|
+
}) => {
|
|
13
|
+
assert(assetClientInterface, 'expected assetClientInterface')
|
|
14
|
+
assert(asset, 'expected asset')
|
|
15
|
+
assert(Number.isInteger(nonce), 'expected integer nonce')
|
|
16
|
+
assert(walletAccount, 'expected walletAccount')
|
|
17
|
+
|
|
18
|
+
// https://github.com/ExodusMovement/assets/blob/fbe3702861cba3b21885a65b15f038fcd8541891/shield/asset-lib/src/balances-utils.js#L26
|
|
19
|
+
const isUnconfirmed = (tx) => !tx.failed && tx.pending
|
|
20
|
+
|
|
21
|
+
const [maybeHighestIncentiveTx] = [
|
|
22
|
+
...(await assetClientInterface.getTxLog({ assetName: asset.name, walletAccount })),
|
|
23
|
+
]
|
|
24
|
+
.filter(isUnconfirmed)
|
|
25
|
+
.filter((tx) => tx.data.nonce === nonce && tx.sent)
|
|
26
|
+
.sort((a, b) => (a.feeAmount.gt(b.feeAmount) ? -1 : b.feeAmount.gt(a.feeAmount) ? 1 : 0))
|
|
27
|
+
|
|
28
|
+
return maybeHighestIncentiveTx
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const getOptimisticTxLogEffects = async ({
|
|
32
|
+
asset,
|
|
33
|
+
amount = asset.currency.ZERO,
|
|
34
|
+
assetClientInterface,
|
|
35
|
+
confirmations = 0,
|
|
36
|
+
feeAmount,
|
|
37
|
+
fromAddress,
|
|
38
|
+
gasLimit,
|
|
39
|
+
nonce,
|
|
40
|
+
txId,
|
|
41
|
+
toAddress,
|
|
42
|
+
tipGasPrice: maybeTipGasPrice,
|
|
43
|
+
walletAccount,
|
|
44
|
+
}) => {
|
|
45
|
+
assert(isNumberUnit(amount), 'expected NumberUnit amount')
|
|
46
|
+
assert(asset, 'expected asset')
|
|
47
|
+
assert(assetClientInterface, 'expected assetClientInterface')
|
|
48
|
+
assert(Number.isInteger(confirmations), 'expected integer confirmations')
|
|
49
|
+
assert(isNumberUnit(feeAmount), 'expected feeAmount')
|
|
50
|
+
assert(typeof fromAddress === 'string', 'expected string fromAddress')
|
|
51
|
+
assert(Number.isInteger(gasLimit), 'expected integer gasLimit')
|
|
52
|
+
assert(Number.isInteger(nonce), 'expected integer nonce')
|
|
53
|
+
assert(typeof toAddress === 'string', 'expected string toAddress')
|
|
54
|
+
assert(txId, 'expected txId')
|
|
55
|
+
assert(walletAccount, 'expected walletAccount')
|
|
56
|
+
|
|
57
|
+
if (maybeTipGasPrice) assert(isNumberUnit(maybeTipGasPrice), 'expected NumberUnit tipGasPrice')
|
|
58
|
+
|
|
59
|
+
const baseAsset = asset.baseAsset
|
|
60
|
+
|
|
61
|
+
// TODO: This is incorrect for token transfers.
|
|
62
|
+
const selfSend = fromAddress.toLowerCase() === toAddress.toLowerCase()
|
|
63
|
+
|
|
64
|
+
assert(asset.feeAsset.name === baseAsset.name, 'inconsistent feeAsset')
|
|
65
|
+
|
|
66
|
+
const maybeTxToReplace = await getHighestIncentivePendingTxByNonce({
|
|
67
|
+
asset,
|
|
68
|
+
assetClientInterface,
|
|
69
|
+
nonce,
|
|
70
|
+
walletAccount,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const replacedTxId =
|
|
74
|
+
maybeTxToReplace && maybeTxToReplace.feeAmount.lt(feeAmount) ? maybeTxToReplace.txId : undefined
|
|
75
|
+
|
|
76
|
+
if (!replacedTxId && maybeTxToReplace) {
|
|
77
|
+
console.log('Attempting to replace a transaction using an insufficient fee!')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const sharedProps = {
|
|
81
|
+
confirmations,
|
|
82
|
+
feeAmount,
|
|
83
|
+
feeCoinName: asset.feeAsset.name,
|
|
84
|
+
selfSend,
|
|
85
|
+
to: toAddress,
|
|
86
|
+
txId,
|
|
87
|
+
data: {
|
|
88
|
+
gasLimit,
|
|
89
|
+
replacedTxId,
|
|
90
|
+
nonce,
|
|
91
|
+
...(maybeTipGasPrice ? { tipGasPrice: maybeTipGasPrice.toBaseString() } : null),
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const optimisticTxLogEffects = [
|
|
96
|
+
{
|
|
97
|
+
assetName: asset.name,
|
|
98
|
+
walletAccount,
|
|
99
|
+
txs: [
|
|
100
|
+
{
|
|
101
|
+
...sharedProps,
|
|
102
|
+
coinAmount: selfSend ? asset.currency.ZERO : amount.abs().negate(),
|
|
103
|
+
coinName: asset.name,
|
|
104
|
+
currencies: {
|
|
105
|
+
[asset.name]: asset.currency,
|
|
106
|
+
[asset.feeAsset.name]: asset.feeAsset.currency,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
isEthereumLikeToken(asset) && {
|
|
112
|
+
assetName: baseAsset.name,
|
|
113
|
+
walletAccount,
|
|
114
|
+
txs: [
|
|
115
|
+
{
|
|
116
|
+
...sharedProps,
|
|
117
|
+
coinAmount: baseAsset.currency.ZERO,
|
|
118
|
+
coinName: baseAsset.name,
|
|
119
|
+
currencies: {
|
|
120
|
+
[baseAsset.name]: baseAsset.currency,
|
|
121
|
+
[asset.feeAsset.name]: asset.feeAsset.currency,
|
|
122
|
+
},
|
|
123
|
+
tokens: [asset.name],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
].filter(Boolean)
|
|
128
|
+
|
|
129
|
+
return { optimisticTxLogEffects }
|
|
130
|
+
}
|
package/src/tx-log/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export { EthereumMonitor } from './ethereum-monitor.js'
|
|
2
2
|
export { EthereumNoHistoryMonitor } from './ethereum-no-history-monitor.js'
|
|
3
3
|
export { ClarityMonitor } from './clarity-monitor.js'
|
|
4
|
+
export {
|
|
5
|
+
getOptimisticTxLogEffects,
|
|
6
|
+
getHighestIncentivePendingTxByNonce,
|
|
7
|
+
} from './get-optimistic-txlog-effects.js'
|
package/src/tx-send/tx-send.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isEthereumLikeToken,
|
|
3
|
-
normalizeTxId,
|
|
4
|
-
parseUnsignedTx,
|
|
5
|
-
updateNonce,
|
|
6
|
-
} from '@exodus/ethereum-lib'
|
|
1
|
+
import { normalizeTxId, parseUnsignedTx, updateNonce } from '@exodus/ethereum-lib'
|
|
7
2
|
import assert from 'minimalistic-assert'
|
|
8
3
|
|
|
9
4
|
import * as ErrorWrapper from '../error-wrapper.js'
|
|
10
5
|
import { transactionExists } from '../eth-like-util.js'
|
|
11
6
|
import { ARBITRARY_ADDRESS } from '../gas-estimation.js'
|
|
7
|
+
import { getOptimisticTxLogEffects } from '../tx-log/index.js'
|
|
12
8
|
|
|
13
9
|
const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
14
10
|
assert(assetClientInterface, 'assetClientInterface is required')
|
|
@@ -25,7 +21,6 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
25
21
|
}
|
|
26
22
|
|
|
27
23
|
return async ({ asset, walletAccount, unsignedTx: providedUnsignedTx, ...legacyParams }) => {
|
|
28
|
-
const assetName = asset.name
|
|
29
24
|
const baseAsset = asset.baseAsset
|
|
30
25
|
|
|
31
26
|
const resolveUnsignedTx = async () => {
|
|
@@ -57,16 +52,13 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
57
52
|
const tipGasPrice = parsedTx.tipGasPrice
|
|
58
53
|
const gasLimit = parsedTx.gasLimit
|
|
59
54
|
const amount = parsedTx.amount
|
|
60
|
-
const
|
|
61
|
-
const eip1559Enabled = parsedTx.eip1559Enabled
|
|
55
|
+
const toAddress = parsedTx.to
|
|
62
56
|
|
|
63
57
|
// unknown data from buffer...
|
|
64
58
|
const fromAddress = unsignedTx.txMeta.fromAddress
|
|
65
|
-
const selfSend = fromAddress === to
|
|
66
|
-
const replacedTxId = unsignedTx.txMeta.bumpTxId
|
|
67
59
|
|
|
68
60
|
assert(
|
|
69
|
-
|
|
61
|
+
toAddress.toLowerCase() !== ARBITRARY_ADDRESS,
|
|
70
62
|
`The receiving wallet address must not be ${ARBITRARY_ADDRESS}`
|
|
71
63
|
)
|
|
72
64
|
|
|
@@ -154,64 +146,23 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
154
146
|
}
|
|
155
147
|
}
|
|
156
148
|
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
await assetClientInterface.updateTxLogAndNotify({
|
|
171
|
-
assetName: asset.name,
|
|
149
|
+
const { optimisticTxLogEffects } = await getOptimisticTxLogEffects({
|
|
150
|
+
amount,
|
|
151
|
+
asset,
|
|
152
|
+
assetClientInterface,
|
|
153
|
+
feeAmount,
|
|
154
|
+
fromAddress,
|
|
155
|
+
gasLimit,
|
|
156
|
+
nonce,
|
|
157
|
+
txId,
|
|
158
|
+
toAddress,
|
|
159
|
+
tipGasPrice,
|
|
172
160
|
walletAccount,
|
|
173
|
-
txs: [
|
|
174
|
-
{
|
|
175
|
-
txId,
|
|
176
|
-
confirmations: 0,
|
|
177
|
-
coinAmount: selfSend ? asset.currency.ZERO : amount.abs().negate(),
|
|
178
|
-
coinName: asset.name,
|
|
179
|
-
feeAmount,
|
|
180
|
-
feeCoinName: asset.feeAsset.name,
|
|
181
|
-
selfSend,
|
|
182
|
-
to,
|
|
183
|
-
currencies: {
|
|
184
|
-
[assetName]: asset.currency,
|
|
185
|
-
[asset.feeAsset.name]: asset.feeAsset.currency,
|
|
186
|
-
},
|
|
187
|
-
data: txData,
|
|
188
|
-
},
|
|
189
|
-
],
|
|
190
161
|
})
|
|
191
162
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
await assetClientInterface.updateTxLogAndNotify(
|
|
195
|
-
assetName: baseAsset.name,
|
|
196
|
-
walletAccount,
|
|
197
|
-
txs: [
|
|
198
|
-
{
|
|
199
|
-
txId,
|
|
200
|
-
coinAmount: baseAsset.currency.ZERO,
|
|
201
|
-
coinName: baseAsset.name,
|
|
202
|
-
feeAmount,
|
|
203
|
-
feeCoinName: baseAsset.name,
|
|
204
|
-
selfSend,
|
|
205
|
-
to,
|
|
206
|
-
token: asset.name,
|
|
207
|
-
currencies: {
|
|
208
|
-
[baseAsset.name]: baseAsset.currency,
|
|
209
|
-
[asset.feeAsset.name]: asset.feeAsset.currency,
|
|
210
|
-
},
|
|
211
|
-
data: txData,
|
|
212
|
-
},
|
|
213
|
-
],
|
|
214
|
-
})
|
|
163
|
+
// NOTE: `optimisticTxLogEffects` **must** be written sequentially.
|
|
164
|
+
for (const optimisticTxLogEffect of optimisticTxLogEffects) {
|
|
165
|
+
await assetClientInterface.updateTxLogAndNotify(optimisticTxLogEffect)
|
|
215
166
|
}
|
|
216
167
|
|
|
217
168
|
return { txId, nonce }
|