@exodus/ethereum-api 8.50.0 → 8.51.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 +24 -0
- package/package.json +2 -2
- package/src/create-asset.js +2 -1
- package/src/tx-create.js +22 -6
- 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 +19 -72
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
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.51.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.50.1...@exodus/ethereum-api@8.51.0) (2025-09-25)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat(ethereum): check Eip1191ChainId in displayAddress function (#6504)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
* fix: removed contractAddress, keepTxInput and and resolve token amount when sending to a different contract (#6508)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## [8.50.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.50.0...@exodus/ethereum-api@8.50.1) (2025-09-22)
|
|
23
|
+
|
|
24
|
+
**Note:** Version bump only for package @exodus/ethereum-api
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
6
30
|
## [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
31
|
|
|
8
32
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.51.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",
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"type": "git",
|
|
67
67
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "a5a5ca09ab04597193c9243306a884df16bdab8b"
|
|
70
70
|
}
|
package/src/create-asset.js
CHANGED
|
@@ -132,7 +132,8 @@ export const createAssetFactory = ({
|
|
|
132
132
|
validate: validateFactory({ chainId, useEip1191ChainIdChecksum }),
|
|
133
133
|
hasChecksum,
|
|
134
134
|
isContract: server.isContract,
|
|
135
|
-
displayAddress: (addr) =>
|
|
135
|
+
displayAddress: (addr) =>
|
|
136
|
+
useEip1191ChainIdChecksum ? toChecksumAddress(addr, chainId) : toChecksumAddress(addr),
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
const bip44 = customBip44 || bip44Constants['ETH']
|
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'
|
|
@@ -200,7 +219,6 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
200
219
|
nft, // when sending nfts
|
|
201
220
|
fromAddress: providedFromAddress, // wallet from address
|
|
202
221
|
toAddress: providedToAddress, // user's to address, not the token or the dex contract
|
|
203
|
-
contractAddress: providedContractAddress, // Provided when swapping a token via the DEX contract, not via the token's contract
|
|
204
222
|
txInput: providedTxInput, // Provided when swapping via a DEX contract
|
|
205
223
|
gasLimit: providedGasLimit, // Provided by exchange when known
|
|
206
224
|
amount: providedAmount, // The NU amount to be sent, to be included in the tx value or tx input
|
|
@@ -211,7 +229,6 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
211
229
|
customFee,
|
|
212
230
|
isSendAll,
|
|
213
231
|
bumpTxId,
|
|
214
|
-
keepTxInput, // @deprecated this flag is used by swaps when swapping a token via DEX. The asset is token but the tx TO address is not the token address. Update swap to use `contractAddress`
|
|
215
232
|
}) => {
|
|
216
233
|
assert(asset, 'asset is required')
|
|
217
234
|
assert(walletAccount, 'walletAccount is required')
|
|
@@ -254,8 +271,7 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
254
271
|
|
|
255
272
|
const resolvedGasPrice = providedGasPrice ?? maybeGasPrice
|
|
256
273
|
|
|
257
|
-
const txToAddress =
|
|
258
|
-
providedContractAddress ?? (isToken && !keepTxInput ? asset.contract.address : toAddress)
|
|
274
|
+
const txToAddress = isToken && !providedTxInput ? asset.contract.address : toAddress
|
|
259
275
|
|
|
260
276
|
const isContractToAddress = await isContractAddressCached({ asset, address: txToAddress })
|
|
261
277
|
|
|
@@ -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 () => {
|
|
@@ -47,26 +42,19 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
47
42
|
// this converts an transactionBuffer to values we can use when creating the tx logs
|
|
48
43
|
const parsedTx = parseUnsignedTx({ asset, unsignedTx })
|
|
49
44
|
|
|
50
|
-
// the txMeta.fee may include implicit l1 fees
|
|
51
|
-
const feeAmount = unsignedTx.txMeta.fee
|
|
52
|
-
? asset.feeAsset.currency.parse(unsignedTx.txMeta.fee)
|
|
53
|
-
: parsedTx.fee
|
|
54
|
-
|
|
55
45
|
let nonce = parsedTx.nonce
|
|
56
46
|
|
|
57
47
|
const tipGasPrice = parsedTx.tipGasPrice
|
|
48
|
+
const feeAmount = parsedTx.fee
|
|
58
49
|
const gasLimit = parsedTx.gasLimit
|
|
59
50
|
const amount = parsedTx.amount
|
|
60
|
-
const
|
|
61
|
-
const eip1559Enabled = parsedTx.eip1559Enabled
|
|
51
|
+
const toAddress = parsedTx.to
|
|
62
52
|
|
|
63
53
|
// unknown data from buffer...
|
|
64
54
|
const fromAddress = unsignedTx.txMeta.fromAddress
|
|
65
|
-
const selfSend = fromAddress === to
|
|
66
|
-
const replacedTxId = unsignedTx.txMeta.bumpTxId
|
|
67
55
|
|
|
68
56
|
assert(
|
|
69
|
-
|
|
57
|
+
toAddress.toLowerCase() !== ARBITRARY_ADDRESS,
|
|
70
58
|
`The receiving wallet address must not be ${ARBITRARY_ADDRESS}`
|
|
71
59
|
)
|
|
72
60
|
|
|
@@ -154,64 +142,23 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
154
142
|
}
|
|
155
143
|
}
|
|
156
144
|
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
await assetClientInterface.updateTxLogAndNotify({
|
|
171
|
-
assetName: asset.name,
|
|
145
|
+
const { optimisticTxLogEffects } = await getOptimisticTxLogEffects({
|
|
146
|
+
amount,
|
|
147
|
+
asset,
|
|
148
|
+
assetClientInterface,
|
|
149
|
+
feeAmount,
|
|
150
|
+
fromAddress,
|
|
151
|
+
gasLimit,
|
|
152
|
+
nonce,
|
|
153
|
+
txId,
|
|
154
|
+
toAddress,
|
|
155
|
+
tipGasPrice,
|
|
172
156
|
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
157
|
})
|
|
191
158
|
|
|
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
|
-
})
|
|
159
|
+
// NOTE: `optimisticTxLogEffects` **must** be written sequentially.
|
|
160
|
+
for (const optimisticTxLogEffect of optimisticTxLogEffects) {
|
|
161
|
+
await assetClientInterface.updateTxLogAndNotify(optimisticTxLogEffect)
|
|
215
162
|
}
|
|
216
163
|
|
|
217
164
|
return { txId, nonce }
|