@exodus/ethereum-api 8.42.2 → 8.43.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 +18 -0
- package/package.json +3 -3
- package/src/create-asset-utils.js +14 -2
- package/src/create-asset.js +10 -3
- package/src/gas-estimation.js +2 -1
- package/src/get-fee-async.js +13 -81
- package/src/get-fee.js +1 -1
- package/src/index.js +1 -3
- package/src/tx-create.js +340 -0
- package/src/tx-send/index.js +0 -1
- package/src/tx-send/nonce-utils.js +1 -1
- package/src/tx-send/tx-send.js +94 -369
- package/src/tx-send/get-fee-info.js +0 -58
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
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.43.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.43.0...@exodus/ethereum-api@8.43.1) (2025-07-17)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @exodus/ethereum-api
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [8.43.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.42.2...@exodus/ethereum-api@8.43.0) (2025-07-14)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
* feat: tx send split, tx-create (#5854)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
## [8.42.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.42.1...@exodus/ethereum-api@8.42.2) (2025-07-11)
|
|
7
25
|
|
|
8
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.43.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",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@exodus/bip44-constants": "^195.0.0",
|
|
29
29
|
"@exodus/crypto": "^1.0.0-rc.13",
|
|
30
30
|
"@exodus/currency": "^6.0.1",
|
|
31
|
-
"@exodus/ethereum-lib": "^5.
|
|
31
|
+
"@exodus/ethereum-lib": "^5.16.0",
|
|
32
32
|
"@exodus/ethereum-meta": "^2.5.0",
|
|
33
33
|
"@exodus/ethereumholesky-meta": "^2.0.2",
|
|
34
34
|
"@exodus/ethereumjs": "^1.0.0",
|
|
@@ -63,5 +63,5 @@
|
|
|
63
63
|
"type": "git",
|
|
64
64
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
65
65
|
},
|
|
66
|
-
"gitHead": "
|
|
66
|
+
"gitHead": "ef192865e8886dedb72c280a1b252ff177e20067"
|
|
67
67
|
}
|
|
@@ -60,6 +60,16 @@ export const resolveMonitorSettings = (
|
|
|
60
60
|
return { ...defaultResolution, monitorType: overrideMonitorType, serverUrl: overrideServerUrl }
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
const stringifyPrivateTx = (tx) => {
|
|
64
|
+
assert(tx, 'expected tx')
|
|
65
|
+
if (tx instanceof Uint8Array) return `0x${Buffer.from(tx).toString('hex')}`
|
|
66
|
+
|
|
67
|
+
assert(typeof tx === 'string', 'expected string')
|
|
68
|
+
if (tx.startsWith('0x')) return tx
|
|
69
|
+
|
|
70
|
+
return `0x${tx}`
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
const broadcastPrivateBundleFactory =
|
|
64
74
|
({ privacyServer }) =>
|
|
65
75
|
async ({ txs }) => {
|
|
@@ -69,7 +79,7 @@ const broadcastPrivateBundleFactory =
|
|
|
69
79
|
await privacyServer.sendRequest(
|
|
70
80
|
privacyServer.buildRequest({
|
|
71
81
|
method: 'eth_sendBundle',
|
|
72
|
-
params: [{ txs }],
|
|
82
|
+
params: [{ txs: txs.map((tx) => stringifyPrivateTx(tx)) }],
|
|
73
83
|
})
|
|
74
84
|
)
|
|
75
85
|
}
|
|
@@ -157,7 +167,7 @@ export const getNonceFactory = ({ assetClientInterface, useAbsoluteBalanceAndNon
|
|
|
157
167
|
assert(assetClientInterface, 'expected assetClientInterface')
|
|
158
168
|
assert(typeof useAbsoluteBalanceAndNonce === 'boolean', 'expected useAbsoluteBalanceAndNonce')
|
|
159
169
|
|
|
160
|
-
const getNonce = async ({ asset, fromAddress, walletAccount }) => {
|
|
170
|
+
const getNonce = async ({ asset, fromAddress, walletAccount, triedNonce, forceFromNode }) => {
|
|
161
171
|
assert(asset, 'expected asset')
|
|
162
172
|
assert(typeof fromAddress === 'string', 'expected fromAddress')
|
|
163
173
|
assert(walletAccount, 'expected walletAccount')
|
|
@@ -171,6 +181,8 @@ export const getNonceFactory = ({ assetClientInterface, useAbsoluteBalanceAndNon
|
|
|
171
181
|
asset,
|
|
172
182
|
fromAddress,
|
|
173
183
|
txLog,
|
|
184
|
+
triedNonce,
|
|
185
|
+
forceFromNode,
|
|
174
186
|
// For assets where we'll fall back to querying the coin node, we
|
|
175
187
|
// search for pending transactions. For base assets with history,
|
|
176
188
|
// we'll fall back to the `TxLog` since this also has a knowledge
|
package/src/create-asset.js
CHANGED
|
@@ -37,6 +37,7 @@ import getFeeAsyncFactory from './get-fee-async.js'
|
|
|
37
37
|
import { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas/index.js'
|
|
38
38
|
import { serverBasedFeeMonitorFactoryFactory } from './server-based-fee-monitor.js'
|
|
39
39
|
import { createStakingApi } from './staking-api.js'
|
|
40
|
+
import { createTxFactory } from './tx-create.js'
|
|
40
41
|
import { txSendFactory } from './tx-send/index.js'
|
|
41
42
|
import { createWeb3API } from './web3/index.js'
|
|
42
43
|
|
|
@@ -221,10 +222,15 @@ export const createAssetFactory = ({
|
|
|
221
222
|
|
|
222
223
|
const createUnsignedTx = createUnsignedTxFactory({ chainId })
|
|
223
224
|
|
|
225
|
+
const createTx = createTxFactory({
|
|
226
|
+
chainId,
|
|
227
|
+
assetClientInterface,
|
|
228
|
+
useAbsoluteNonce: useAbsoluteBalanceAndNonce,
|
|
229
|
+
})
|
|
230
|
+
|
|
224
231
|
const sendTx = txSendFactory({
|
|
225
232
|
assetClientInterface,
|
|
226
|
-
|
|
227
|
-
useAbsoluteBalanceAndNonce,
|
|
233
|
+
createTx,
|
|
228
234
|
})
|
|
229
235
|
|
|
230
236
|
const estimateL1DataFee = l1GasOracleAddress
|
|
@@ -246,6 +252,7 @@ export const createAssetFactory = ({
|
|
|
246
252
|
createFeeMonitor,
|
|
247
253
|
createHistoryMonitor,
|
|
248
254
|
createToken,
|
|
255
|
+
createTx,
|
|
249
256
|
createUnsignedTx,
|
|
250
257
|
customFees: createCustomFeesApi({ baseAsset: asset }),
|
|
251
258
|
defaultAddressPath,
|
|
@@ -255,7 +262,7 @@ export const createAssetFactory = ({
|
|
|
255
262
|
getBalanceForAddress: createGetBalanceForAddress({ asset, server }),
|
|
256
263
|
getConfirmationsNumber: () => confirmationsNumber,
|
|
257
264
|
getDefaultAddressPath: () => defaultAddressPath,
|
|
258
|
-
getFeeAsync: getFeeAsyncFactory({ assetClientInterface, gasLimit,
|
|
265
|
+
getFeeAsync: getFeeAsyncFactory({ assetClientInterface, gasLimit, createTx }),
|
|
259
266
|
getFee,
|
|
260
267
|
getFeeData: () => feeData,
|
|
261
268
|
getKeyIdentifier: createGetKeyIdentifier({
|
package/src/gas-estimation.js
CHANGED
|
@@ -114,6 +114,7 @@ export async function fetchGasLimit({
|
|
|
114
114
|
toAddress: providedToAddress,
|
|
115
115
|
txInput: providedTxInput,
|
|
116
116
|
amount: providedAmount,
|
|
117
|
+
contractAddress,
|
|
117
118
|
bip70,
|
|
118
119
|
throwOnError = true,
|
|
119
120
|
}) {
|
|
@@ -146,7 +147,7 @@ export async function fetchGasLimit({
|
|
|
146
147
|
toAddress: providedToAddress,
|
|
147
148
|
})
|
|
148
149
|
|
|
149
|
-
const txToAddress = isToken ? asset.contract.address : toAddress
|
|
150
|
+
const txToAddress = contractAddress ?? (isToken ? asset.contract.address : toAddress)
|
|
150
151
|
const txAmount = isToken ? asset.baseAsset.currency.ZERO : amount
|
|
151
152
|
|
|
152
153
|
try {
|
package/src/get-fee-async.js
CHANGED
|
@@ -1,89 +1,21 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { getFeeFactory } from './get-fee.js'
|
|
5
|
-
import { getNftArguments } from './nft-utils.js'
|
|
3
|
+
import { getExtraFeeData } from './get-fee.js'
|
|
6
4
|
|
|
7
|
-
const getFeeAsyncFactory = ({
|
|
8
|
-
assetClientInterface,
|
|
9
|
-
gasLimit: defaultGasLimit,
|
|
10
|
-
createUnsignedTx,
|
|
11
|
-
}) => {
|
|
5
|
+
const getFeeAsyncFactory = ({ assetClientInterface, createTx }) => {
|
|
12
6
|
assert(assetClientInterface, 'assetClientInterface is required')
|
|
13
|
-
assert(
|
|
14
|
-
|
|
15
|
-
return async ({
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
amount: providedAmount,
|
|
22
|
-
txInput: providedTxInput,
|
|
23
|
-
gasLimit: providedGasLimit,
|
|
24
|
-
bip70,
|
|
25
|
-
customFee,
|
|
26
|
-
feeData,
|
|
27
|
-
}) => {
|
|
28
|
-
const fromAddress = providedFromAddress || ARBITRARY_ADDRESS // sending from a random address
|
|
29
|
-
const toAddress = providedToAddress || ARBITRARY_ADDRESS // sending to a random address,
|
|
30
|
-
const amount = providedAmount ?? asset.currency.ZERO
|
|
31
|
-
const resolveGasLimit = async () => {
|
|
32
|
-
if (nft) {
|
|
33
|
-
return getNftArguments({ asset, nft, fromAddress, toAddress })
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const txInput = providedTxInput || resolveDefaultTxInput({ asset, toAddress, amount })
|
|
37
|
-
if (providedGasLimit) return { gasLimit: providedGasLimit, txInput }
|
|
38
|
-
|
|
39
|
-
const gasLimit = await fetchGasLimit({
|
|
40
|
-
asset,
|
|
41
|
-
fromAddress: providedFromAddress,
|
|
42
|
-
toAddress: providedToAddress,
|
|
43
|
-
txInput,
|
|
44
|
-
amount,
|
|
45
|
-
bip70,
|
|
46
|
-
feeData,
|
|
47
|
-
})
|
|
48
|
-
return { gasLimit, txInput }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const { txInput, gasLimit, contractAddress } = await resolveGasLimit()
|
|
52
|
-
|
|
53
|
-
const { fee, gasPrice, ...rest } = getFee({
|
|
54
|
-
asset,
|
|
55
|
-
feeData,
|
|
56
|
-
gasLimit,
|
|
57
|
-
amount,
|
|
58
|
-
customFee,
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
const optimismL1DataFee = asset.baseAsset.estimateL1DataFee
|
|
62
|
-
? await asset.baseAsset.estimateL1DataFee({
|
|
63
|
-
unsignedTx: createUnsignedTx({
|
|
64
|
-
asset,
|
|
65
|
-
address: contractAddress || toAddress,
|
|
66
|
-
fromAddress,
|
|
67
|
-
amount,
|
|
68
|
-
nonce: 0,
|
|
69
|
-
txInput,
|
|
70
|
-
gasPrice,
|
|
71
|
-
gasLimit,
|
|
72
|
-
}),
|
|
73
|
-
})
|
|
74
|
-
: undefined
|
|
75
|
-
|
|
76
|
-
const l1DataFee = optimismL1DataFee
|
|
77
|
-
? asset.baseAsset.currency.baseUnit(optimismL1DataFee)
|
|
78
|
-
: asset.baseAsset.currency.ZERO
|
|
79
|
-
|
|
7
|
+
assert(createTx, 'createTx is required')
|
|
8
|
+
|
|
9
|
+
return async (params) => {
|
|
10
|
+
const { asset } = params
|
|
11
|
+
const { unsignedTx } = params.unsignedTx ? params : await createTx(params)
|
|
12
|
+
const fee = asset.feeAsset.currency.parse(unsignedTx.txMeta.fee)
|
|
13
|
+
const coinAmount = asset.currency.parse(unsignedTx.txMeta.amount)
|
|
14
|
+
const extraFeeData = getExtraFeeData({ asset, amount: coinAmount })
|
|
80
15
|
return {
|
|
81
|
-
fee
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
gasLimit,
|
|
85
|
-
gasPrice,
|
|
86
|
-
...rest,
|
|
16
|
+
fee,
|
|
17
|
+
extraFeeData,
|
|
18
|
+
unsignedTx,
|
|
87
19
|
}
|
|
88
20
|
}
|
|
89
21
|
}
|
package/src/get-fee.js
CHANGED
package/src/index.js
CHANGED
|
@@ -83,9 +83,7 @@ export {
|
|
|
83
83
|
|
|
84
84
|
export { reasons as errorReasons, withErrorReason, EthLikeError } from './error-wrapper.js'
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
// be considered an internal API.
|
|
88
|
-
export { txSendFactory, getFeeInfo } from './tx-send/index.js'
|
|
86
|
+
export { txSendFactory } from './tx-send/index.js'
|
|
89
87
|
|
|
90
88
|
export { createAssetFactory } from './create-asset.js'
|
|
91
89
|
|
package/src/tx-create.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { calculateBumpedGasPrice, currency2buffer, isEthereumLikeToken } from '@exodus/ethereum-lib'
|
|
2
|
+
import createEthereumJsTx from '@exodus/ethereum-lib/src/unsigned-tx/create-ethereumjs-tx.js'
|
|
3
|
+
import assert from 'minimalistic-assert'
|
|
4
|
+
|
|
5
|
+
import * as ErrorWrapper from './error-wrapper.js'
|
|
6
|
+
import { isContractAddressCached } from './eth-like-util.js'
|
|
7
|
+
import { ensureSaneEip1559GasPriceForTipGasPrice } from './fee-utils.js'
|
|
8
|
+
import { ARBITRARY_ADDRESS, fetchGasLimit, resolveDefaultTxInput } from './gas-estimation.js'
|
|
9
|
+
import { getFeeFactoryGasPrices } from './get-fee.js'
|
|
10
|
+
import { getNftArguments } from './nft-utils.js'
|
|
11
|
+
|
|
12
|
+
async function createUnsignedTxWithFees({
|
|
13
|
+
asset,
|
|
14
|
+
chainId,
|
|
15
|
+
to, // the tx to address, it could be the reciver address for native sending, the token contract, the DEX contract, etc
|
|
16
|
+
value, // the value of the tx in NU, it can be the value in eth or 0 when calling contracts
|
|
17
|
+
data, // the data of the tx in hex string, it can be 0x for native sending or the params when sending to a contract
|
|
18
|
+
gasLimit,
|
|
19
|
+
eip1559Enabled,
|
|
20
|
+
gasPrice, // eip 1559: `maxFeePerGas`
|
|
21
|
+
tipGasPrice, // eip 1559: `maxPriorityPerGas`
|
|
22
|
+
nonce,
|
|
23
|
+
bumpTxId,
|
|
24
|
+
coinAmount, // coinAmount
|
|
25
|
+
fromAddress, // user's sending address
|
|
26
|
+
toAddress, // user's receiver address
|
|
27
|
+
}) {
|
|
28
|
+
assert(asset, 'asset is required')
|
|
29
|
+
assert(typeof chainId === 'number', 'chainId is required')
|
|
30
|
+
assert(to, 'to is required')
|
|
31
|
+
assert(value, 'value is required')
|
|
32
|
+
assert(data, 'data is required')
|
|
33
|
+
assert(gasLimit, 'gasLimit is required')
|
|
34
|
+
assert(gasPrice, 'gasPrice is required')
|
|
35
|
+
assert(coinAmount, 'coinAmount is required')
|
|
36
|
+
assert(fromAddress, 'fromAddress is required')
|
|
37
|
+
assert(toAddress, 'toAddress is required')
|
|
38
|
+
assert(typeof eip1559Enabled === 'boolean', 'eip1559Enabled is required')
|
|
39
|
+
|
|
40
|
+
const ethjsTx = createEthereumJsTx({
|
|
41
|
+
txData: {
|
|
42
|
+
nonce,
|
|
43
|
+
gasPrice: currency2buffer(gasPrice),
|
|
44
|
+
tipGasPrice: tipGasPrice ? currency2buffer(tipGasPrice) : undefined,
|
|
45
|
+
gasLimit,
|
|
46
|
+
to,
|
|
47
|
+
value: currency2buffer(value),
|
|
48
|
+
data,
|
|
49
|
+
chainId,
|
|
50
|
+
},
|
|
51
|
+
txMeta: {
|
|
52
|
+
eip1559Enabled,
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
const transactionBuffer = ethjsTx.serialize()
|
|
56
|
+
|
|
57
|
+
const baseFee = gasPrice.mul(gasLimit)
|
|
58
|
+
const optimismL1DataFee = asset.baseAsset.estimateL1DataFee
|
|
59
|
+
? await asset.baseAsset.estimateL1DataFee({
|
|
60
|
+
unsignedTx: { txData: { transactionBuffer, chainId } },
|
|
61
|
+
})
|
|
62
|
+
: undefined
|
|
63
|
+
|
|
64
|
+
const l1DataFee = optimismL1DataFee
|
|
65
|
+
? asset.baseAsset.currency.baseUnit(optimismL1DataFee)
|
|
66
|
+
: asset.baseAsset.currency.ZERO
|
|
67
|
+
|
|
68
|
+
const fee = baseFee.add(l1DataFee)
|
|
69
|
+
|
|
70
|
+
const unsignedTx = {
|
|
71
|
+
txData: { transactionBuffer, chainId },
|
|
72
|
+
txMeta: {
|
|
73
|
+
bumpTxId,
|
|
74
|
+
eip1559Enabled,
|
|
75
|
+
fromAddress,
|
|
76
|
+
toAddress,
|
|
77
|
+
amount: coinAmount.toDefaultString({ unit: true }),
|
|
78
|
+
fee: fee.toDefaultString({ unit: true }),
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
return { unsignedTx }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const createBumpUnsignedTx = async ({
|
|
85
|
+
fromAddress,
|
|
86
|
+
chainId,
|
|
87
|
+
asset,
|
|
88
|
+
bumpTxId,
|
|
89
|
+
baseAssetTxLog,
|
|
90
|
+
assetClientInterface,
|
|
91
|
+
walletAccount,
|
|
92
|
+
feeData,
|
|
93
|
+
nonce: providedNonce,
|
|
94
|
+
}) => {
|
|
95
|
+
const baseAsset = asset.baseAsset
|
|
96
|
+
const replacedTx = baseAssetTxLog.get(bumpTxId)
|
|
97
|
+
const assets = await assetClientInterface.getAssetsForNetwork({ baseAssetName: baseAsset.name })
|
|
98
|
+
if (!replacedTx || !replacedTx.pending) {
|
|
99
|
+
throw new Error(`Cannot bump transaction ${bumpTxId}: not found or confirmed`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let replacedTokenTx
|
|
103
|
+
if (replacedTx.tokens.length > 0) {
|
|
104
|
+
const [tokenAssetName] = replacedTx.tokens
|
|
105
|
+
const tokenTxSet = await assetClientInterface.getTxLog({
|
|
106
|
+
assetName: tokenAssetName,
|
|
107
|
+
walletAccount,
|
|
108
|
+
})
|
|
109
|
+
replacedTokenTx = tokenTxSet.get(bumpTxId)
|
|
110
|
+
|
|
111
|
+
if (replacedTokenTx) {
|
|
112
|
+
// Attempt to overwrite the asset to reflect the fact that
|
|
113
|
+
// we're performing a token transaction.
|
|
114
|
+
asset = assets[tokenAssetName]
|
|
115
|
+
if (!asset) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`unable to find ${tokenAssetName} during token bump transaction: asset was not available in assetsForNetwork`
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const toAddress = (replacedTokenTx || replacedTx).to
|
|
124
|
+
const isToken = isEthereumLikeToken(asset)
|
|
125
|
+
const txToAddress = isToken ? asset.contract.address : toAddress
|
|
126
|
+
const coinAmount = (replacedTokenTx || replacedTx).coinAmount.negate()
|
|
127
|
+
const gasLimit = replacedTx.data.gasLimit
|
|
128
|
+
|
|
129
|
+
const value = isToken ? baseAsset.currency.ZERO : coinAmount
|
|
130
|
+
|
|
131
|
+
const {
|
|
132
|
+
gasPrice: currentGasPrice,
|
|
133
|
+
baseFeePerGas: currentBaseFee,
|
|
134
|
+
eip1559Enabled,
|
|
135
|
+
tipGasPrice: currentTipGasPrice,
|
|
136
|
+
} = feeData
|
|
137
|
+
const { bumpedGasPrice, bumpedTipGasPrice } = calculateBumpedGasPrice({
|
|
138
|
+
baseAsset,
|
|
139
|
+
tx: replacedTx,
|
|
140
|
+
currentGasPrice,
|
|
141
|
+
currentBaseFee,
|
|
142
|
+
currentTipGasPrice,
|
|
143
|
+
eip1559Enabled,
|
|
144
|
+
})
|
|
145
|
+
const gasPrice = bumpedGasPrice
|
|
146
|
+
const tipGasPrice = bumpedTipGasPrice
|
|
147
|
+
const nonce = replacedTx.data.nonce
|
|
148
|
+
const data = replacedTokenTx
|
|
149
|
+
? asset.contract.transfer.build(toAddress.toLowerCase(), coinAmount.toBaseString())
|
|
150
|
+
: replacedTx.data.data || '0x'
|
|
151
|
+
|
|
152
|
+
if (nonce === undefined) {
|
|
153
|
+
throw new Error(`Cannot bump transaction ${bumpTxId}: data object seems to be corrupted`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// If we have evaluated a bump transaction and the `providedNonce` differs
|
|
157
|
+
// from the `bumpNonce`, we've encountered a conflict and cannot respect
|
|
158
|
+
// the caller's request.
|
|
159
|
+
if (typeof nonce === 'number' && typeof providedNonce === 'number' && nonce !== providedNonce)
|
|
160
|
+
throw new ErrorWrapper.EthLikeError({
|
|
161
|
+
message: new Error('incorrect nonce for replacement transaction'),
|
|
162
|
+
reason: ErrorWrapper.reasons.bumpTxFailed,
|
|
163
|
+
hint: 'providedNonce',
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
return createUnsignedTxWithFees({
|
|
167
|
+
asset,
|
|
168
|
+
chainId,
|
|
169
|
+
to: txToAddress,
|
|
170
|
+
value,
|
|
171
|
+
data,
|
|
172
|
+
gasLimit,
|
|
173
|
+
gasPrice,
|
|
174
|
+
tipGasPrice,
|
|
175
|
+
nonce,
|
|
176
|
+
bumpTxId,
|
|
177
|
+
coinAmount,
|
|
178
|
+
fromAddress,
|
|
179
|
+
toAddress,
|
|
180
|
+
eip1559Enabled,
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonce }) => {
|
|
185
|
+
assert(assetClientInterface, 'assetClientInterface is required')
|
|
186
|
+
assert(typeof chainId === 'number', 'chainId is required')
|
|
187
|
+
return async ({
|
|
188
|
+
asset,
|
|
189
|
+
walletAccount,
|
|
190
|
+
feeData,
|
|
191
|
+
nft, // when sending nfts
|
|
192
|
+
fromAddress: providedFromAddress, // wallet from address
|
|
193
|
+
toAddress: providedToAddress, // user's to address, not the token or the dex contract
|
|
194
|
+
contractAddress: providedContractAddress, // Provided when swapping a token via the DEX contract, not via the token's contract
|
|
195
|
+
txInput: providedTxInput, // Provided when swapping via a DEX contract
|
|
196
|
+
gasLimit: providedGasLimit, // Provided by exchange when known
|
|
197
|
+
amount: providedAmount, // The NU amount to be sent, to be included in the tx value or tx input
|
|
198
|
+
nonce: providedNonce,
|
|
199
|
+
tipGasPrice: providedTipGasPrice,
|
|
200
|
+
gasPrice: providedGasPrice,
|
|
201
|
+
bip70,
|
|
202
|
+
isExchange,
|
|
203
|
+
customFee,
|
|
204
|
+
isSendAll,
|
|
205
|
+
bumpTxId,
|
|
206
|
+
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`
|
|
207
|
+
}) => {
|
|
208
|
+
assert(asset, 'asset is required')
|
|
209
|
+
assert(feeData, 'feeData is required')
|
|
210
|
+
const fromAddress = providedFromAddress || ARBITRARY_ADDRESS
|
|
211
|
+
|
|
212
|
+
const baseAssetTxLog = await assetClientInterface.getTxLog({
|
|
213
|
+
assetName: asset.baseAsset.name,
|
|
214
|
+
walletAccount,
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
if (bumpTxId) {
|
|
218
|
+
return createBumpUnsignedTx({
|
|
219
|
+
chainId,
|
|
220
|
+
asset,
|
|
221
|
+
fromAddress,
|
|
222
|
+
bumpTxId,
|
|
223
|
+
baseAssetTxLog,
|
|
224
|
+
assetClientInterface,
|
|
225
|
+
walletAccount,
|
|
226
|
+
feeData,
|
|
227
|
+
nonce: providedNonce,
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const toAddress = providedToAddress || ARBITRARY_ADDRESS
|
|
232
|
+
const {
|
|
233
|
+
gasPrice: maybeGasPrice,
|
|
234
|
+
feeData: { tipGasPrice: maybeTipGasPrice, eip1559Enabled },
|
|
235
|
+
} = getFeeFactoryGasPrices({ customFee, feeData })
|
|
236
|
+
|
|
237
|
+
const isToken = isEthereumLikeToken(asset)
|
|
238
|
+
|
|
239
|
+
const resolvedGasPrice = providedGasPrice ?? maybeGasPrice
|
|
240
|
+
|
|
241
|
+
const txToAddress =
|
|
242
|
+
providedContractAddress ?? (isToken && !keepTxInput ? asset.contract.address : toAddress)
|
|
243
|
+
|
|
244
|
+
const isContractToAddress = await isContractAddressCached({ asset, address: txToAddress })
|
|
245
|
+
|
|
246
|
+
// HACK: We cannot ensure the no dust invariant for `isSendAll`
|
|
247
|
+
// transactions to contract addresses, since we may be
|
|
248
|
+
// performing a raw token transaction and the parameter
|
|
249
|
+
// applies to the token and not the native amount.
|
|
250
|
+
//
|
|
251
|
+
// Contracts have nondeterministic gas most of the time
|
|
252
|
+
// versus estimations, anyway.
|
|
253
|
+
const isSendAllBaseAsset = isSendAll && !isToken && !isContractToAddress
|
|
254
|
+
|
|
255
|
+
// For native send all transactions, we have to make sure that
|
|
256
|
+
// the `tipGasPrice` is equal to the `gasPrice`, since this is
|
|
257
|
+
// effectively like saying that the `maxFeePerGas` is equal
|
|
258
|
+
// to the `maxPriorityFeePerGas`. We do this so that for a
|
|
259
|
+
// fixed gas cost transaction, no dust balance should remain,
|
|
260
|
+
// since any deviation in the underlying `baseFeePerGas` will
|
|
261
|
+
// result only affect the tip for the miner - no dust remains.
|
|
262
|
+
const tipGasPrice =
|
|
263
|
+
providedTipGasPrice ??
|
|
264
|
+
(eip1559Enabled && isSendAllBaseAsset ? resolvedGasPrice : maybeTipGasPrice)
|
|
265
|
+
|
|
266
|
+
const gasPrice = eip1559Enabled
|
|
267
|
+
? ensureSaneEip1559GasPriceForTipGasPrice({
|
|
268
|
+
gasPrice: resolvedGasPrice,
|
|
269
|
+
tipGasPrice,
|
|
270
|
+
})
|
|
271
|
+
: resolvedGasPrice
|
|
272
|
+
|
|
273
|
+
const nonce =
|
|
274
|
+
providedNonce ?? (await asset.baseAsset.getNonce({ asset, fromAddress, walletAccount }))
|
|
275
|
+
|
|
276
|
+
if (nft) {
|
|
277
|
+
const {
|
|
278
|
+
contractAddress: txToAddress,
|
|
279
|
+
gasLimit,
|
|
280
|
+
txInput,
|
|
281
|
+
} = await getNftArguments({
|
|
282
|
+
asset,
|
|
283
|
+
nft,
|
|
284
|
+
fromAddress,
|
|
285
|
+
toAddress: providedToAddress,
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
const value = asset.baseAsset.currency.ZERO
|
|
289
|
+
|
|
290
|
+
return createUnsignedTxWithFees({
|
|
291
|
+
chainId,
|
|
292
|
+
asset,
|
|
293
|
+
to: txToAddress,
|
|
294
|
+
value,
|
|
295
|
+
data: txInput,
|
|
296
|
+
gasLimit,
|
|
297
|
+
gasPrice,
|
|
298
|
+
tipGasPrice,
|
|
299
|
+
nonce,
|
|
300
|
+
coinAmount: value,
|
|
301
|
+
fromAddress,
|
|
302
|
+
toAddress,
|
|
303
|
+
eip1559Enabled,
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const amount = providedAmount ?? asset.currency.ZERO
|
|
308
|
+
|
|
309
|
+
const value = isToken ? asset.baseAsset.currency.ZERO : amount
|
|
310
|
+
const txInput = providedTxInput || resolveDefaultTxInput({ asset, toAddress, amount })
|
|
311
|
+
const gasLimit =
|
|
312
|
+
providedGasLimit ??
|
|
313
|
+
(await fetchGasLimit({
|
|
314
|
+
asset,
|
|
315
|
+
feeData,
|
|
316
|
+
fromAddress: providedFromAddress,
|
|
317
|
+
toAddress: providedToAddress,
|
|
318
|
+
txInput: providedTxInput,
|
|
319
|
+
contractAddress: txToAddress,
|
|
320
|
+
bip70,
|
|
321
|
+
amount,
|
|
322
|
+
}))
|
|
323
|
+
|
|
324
|
+
return createUnsignedTxWithFees({
|
|
325
|
+
asset,
|
|
326
|
+
chainId,
|
|
327
|
+
to: txToAddress,
|
|
328
|
+
value,
|
|
329
|
+
data: txInput,
|
|
330
|
+
gasLimit,
|
|
331
|
+
gasPrice,
|
|
332
|
+
tipGasPrice,
|
|
333
|
+
nonce,
|
|
334
|
+
coinAmount: amount,
|
|
335
|
+
fromAddress,
|
|
336
|
+
toAddress,
|
|
337
|
+
eip1559Enabled,
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
}
|
package/src/tx-send/index.js
CHANGED
package/src/tx-send/tx-send.js
CHANGED
|
@@ -1,230 +1,97 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
isEthereumLikeToken,
|
|
3
|
+
normalizeTxId,
|
|
4
|
+
parseUnsignedTx,
|
|
5
|
+
updateNonce,
|
|
6
|
+
} from '@exodus/ethereum-lib'
|
|
4
7
|
import assert from 'minimalistic-assert'
|
|
5
8
|
|
|
6
9
|
import * as ErrorWrapper from '../error-wrapper.js'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import getFeeInfo from './get-fee-info.js'
|
|
10
|
-
import { resolveNonce } from './nonce-utils.js'
|
|
11
|
-
|
|
12
|
-
// Exodus enforces a strict invariant that `sendAll` transactions
|
|
13
|
-
// must not leave any dust in the sender's account. Currently, the
|
|
14
|
-
// assets library has the expectation that the client frontend
|
|
15
|
-
// should calculate the precise amount to send, but due to the
|
|
16
|
-
// sheer amount of variables involved when resolving a `gasPrice`,
|
|
17
|
-
// this is a significant undertaking.
|
|
18
|
-
//
|
|
19
|
-
// Therefore, although clients try their very best to calculate
|
|
20
|
-
// the correct amount, in cases this fails we can fall back to
|
|
21
|
-
// the implementation defined here with a warning.
|
|
22
|
-
// eslint-disable-next-line camelcase
|
|
23
|
-
export const HACK_maybeRefineSendAllAmount = async ({
|
|
24
|
-
amount: providedAmount,
|
|
25
|
-
asset,
|
|
26
|
-
assetClientInterface,
|
|
27
|
-
walletAccount,
|
|
28
|
-
gasLimit,
|
|
29
|
-
gasPrice,
|
|
30
|
-
}) => {
|
|
31
|
-
try {
|
|
32
|
-
const { name: assetName, estimateL1DataFee } = asset
|
|
33
|
-
|
|
34
|
-
// HACK: For the interim, we won't attempt to
|
|
35
|
-
// reconcile transaction dust on L2s due
|
|
36
|
-
// to the nondeterminism about the calldata
|
|
37
|
-
// fee.
|
|
38
|
-
if (typeof estimateL1DataFee === 'function') return null
|
|
39
|
-
|
|
40
|
-
const [txLog, accountState] = await Promise.all([
|
|
41
|
-
assetClientInterface.getTxLog({
|
|
42
|
-
assetName,
|
|
43
|
-
walletAccount,
|
|
44
|
-
}),
|
|
45
|
-
assetClientInterface.getAccountState({
|
|
46
|
-
assetName,
|
|
47
|
-
walletAccount,
|
|
48
|
-
}),
|
|
49
|
-
])
|
|
50
|
-
|
|
51
|
-
const { spendable } = await asset.api.getBalances({ asset, txLog, accountState })
|
|
52
|
-
const maxGasCost = gasPrice.mul(gasLimit)
|
|
53
|
-
|
|
54
|
-
if (maxGasCost.gt(spendable)) throw new Error('transaction gas cost exceeds spendable balance')
|
|
10
|
+
import { transactionExists } from '../eth-like-util.js'
|
|
11
|
+
import { ARBITRARY_ADDRESS } from '../gas-estimation.js'
|
|
55
12
|
|
|
56
|
-
|
|
13
|
+
const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
14
|
+
assert(assetClientInterface, 'assetClientInterface is required')
|
|
15
|
+
assert(createTx, 'createTx is required')
|
|
57
16
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
17
|
+
async function signTx({ asset, unsignedTx, walletAccount }) {
|
|
18
|
+
const { rawTx, txId } = await assetClientInterface.signTransaction({
|
|
19
|
+
assetName: asset.baseAsset.name,
|
|
20
|
+
unsignedTx,
|
|
21
|
+
walletAccount,
|
|
22
|
+
})
|
|
61
23
|
|
|
62
|
-
|
|
63
|
-
return expectedSendAllAmount
|
|
64
|
-
} catch (e) {
|
|
65
|
-
console.error('failed to refine send all amount', e)
|
|
66
|
-
return null
|
|
24
|
+
return { rawTx, txId: normalizeTxId(txId) }
|
|
67
25
|
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBalanceAndNonce }) => {
|
|
71
|
-
assert(assetClientInterface, 'assetClientInterface is required')
|
|
72
|
-
assert(createUnsignedTx, 'createUnsignedTx is required')
|
|
73
|
-
return async ({ asset, walletAccount, address, amount, feeData: maybeFeeData, options = {} }) => {
|
|
74
|
-
const {
|
|
75
|
-
nft,
|
|
76
|
-
bumpTxId,
|
|
77
|
-
nonce: providedNonce,
|
|
78
|
-
customFee,
|
|
79
|
-
keepTxInput,
|
|
80
|
-
isSendAll,
|
|
81
|
-
isHardware,
|
|
82
|
-
isPrivate,
|
|
83
|
-
} = options
|
|
84
|
-
let { txInput, feeAmount: providedFeeAmount } = options // avoid let!
|
|
85
|
-
|
|
86
|
-
const feeOpts = {
|
|
87
|
-
gasPrice: options.gasPrice,
|
|
88
|
-
tipGasPrice: options.tipGasPrice,
|
|
89
|
-
gasLimit: options.gasLimit,
|
|
90
|
-
}
|
|
91
26
|
|
|
27
|
+
return async ({ asset, walletAccount, unsignedTx: providedUnsignedTx, ...legacyParams }) => {
|
|
92
28
|
const assetName = asset.name
|
|
93
29
|
const baseAsset = asset.baseAsset
|
|
94
30
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
(await assetClientInterface.getFeeData({
|
|
100
|
-
assetName: baseAsset.name,
|
|
101
|
-
}))
|
|
31
|
+
const resolveUnsignedTx = async () => {
|
|
32
|
+
if (providedUnsignedTx) {
|
|
33
|
+
return { unsignedTx: providedUnsignedTx }
|
|
34
|
+
}
|
|
102
35
|
|
|
103
|
-
|
|
36
|
+
const feeData =
|
|
37
|
+
legacyParams.feeData ??
|
|
38
|
+
(await assetClientInterface.getFeeData({
|
|
39
|
+
assetName: baseAsset.name,
|
|
40
|
+
}))
|
|
104
41
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
42
|
+
const fromAddress =
|
|
43
|
+
legacyParams.fromAddress ??
|
|
44
|
+
(await assetClientInterface.getReceiveAddress({
|
|
45
|
+
assetName: baseAsset.name,
|
|
46
|
+
walletAccount,
|
|
47
|
+
}))
|
|
109
48
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
49
|
+
return createTx({
|
|
50
|
+
asset,
|
|
51
|
+
walletAccount,
|
|
52
|
+
feeData,
|
|
53
|
+
fromAddress,
|
|
54
|
+
toAddress: legacyParams.address,
|
|
55
|
+
...legacyParams,
|
|
56
|
+
...legacyParams.options,
|
|
57
|
+
})
|
|
117
58
|
}
|
|
118
59
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const baseAssetTxLog = await assetClientInterface.getTxLog({
|
|
122
|
-
assetName: baseAsset.name,
|
|
123
|
-
walletAccount,
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
// `replacedTx` is always an ETH/ETC transaction (not a token)
|
|
127
|
-
let replacedTx, replacedTokenTx
|
|
128
|
-
if (bumpTxId) {
|
|
129
|
-
replacedTx = baseAssetTxLog.get(bumpTxId)
|
|
60
|
+
const { unsignedTx } = await resolveUnsignedTx()
|
|
130
61
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
62
|
+
// this converts an transactionBuffer to values we can use when creating the tx logs
|
|
63
|
+
const parsedTx = parseUnsignedTx({ asset, unsignedTx })
|
|
134
64
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
walletAccount,
|
|
140
|
-
})
|
|
141
|
-
replacedTokenTx = tokenTxSet.get(bumpTxId)
|
|
65
|
+
// the txMeta.fee may include implicit l1 fees
|
|
66
|
+
const feeAmount = unsignedTx.txMeta.fee
|
|
67
|
+
? asset.feeAsset.currency.parse(unsignedTx.txMeta.fee)
|
|
68
|
+
: parsedTx.fee
|
|
142
69
|
|
|
143
|
-
|
|
144
|
-
// Attempt to overwrite the asset to reflect the fact that
|
|
145
|
-
// we're performing a token transaction.
|
|
146
|
-
asset = assets[tokenAssetName]
|
|
147
|
-
if (!asset) {
|
|
148
|
-
console.warn(
|
|
149
|
-
`unable to find ${tokenAssetName} during token bump transaction: asset was not available in assetsForNetwork`
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
}
|
|
70
|
+
let nonce = parsedTx.nonce
|
|
153
71
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
feeOpts.gasLimit = replacedTx.data.gasLimit
|
|
72
|
+
const tipGasPrice = parsedTx.tipGasPrice
|
|
73
|
+
const gasLimit = parsedTx.gasLimit
|
|
74
|
+
const amount = parsedTx.amount
|
|
75
|
+
const to = parsedTx.to
|
|
76
|
+
const eip1559Enabled = parsedTx.eip1559Enabled
|
|
160
77
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
} = feeData
|
|
166
|
-
const { bumpedGasPrice, bumpedTipGasPrice } = calculateBumpedGasPrice({
|
|
167
|
-
baseAsset,
|
|
168
|
-
tx: replacedTx,
|
|
169
|
-
currentGasPrice,
|
|
170
|
-
currentBaseFee,
|
|
171
|
-
currentTipGasPrice,
|
|
172
|
-
eip1559Enabled,
|
|
173
|
-
})
|
|
174
|
-
feeOpts.gasPrice = bumpedGasPrice
|
|
175
|
-
feeOpts.tipGasPrice = bumpedTipGasPrice
|
|
176
|
-
bumpNonce = replacedTx.data.nonce
|
|
177
|
-
txInput = replacedTokenTx ? null : replacedTx.data.data || '0x'
|
|
178
|
-
if (bumpNonce === undefined) {
|
|
179
|
-
throw new Error(`Cannot bump transaction ${bumpTxId}: data object seems to be corrupted`)
|
|
180
|
-
}
|
|
181
|
-
}
|
|
78
|
+
// unknown data from buffer...
|
|
79
|
+
const fromAddress = unsignedTx.txMeta.fromAddress
|
|
80
|
+
const selfSend = fromAddress === to
|
|
81
|
+
const replacedTxId = unsignedTx.txMeta.bumpTxId
|
|
182
82
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (
|
|
187
|
-
typeof bumpNonce === 'number' &&
|
|
188
|
-
typeof providedNonce === 'number' &&
|
|
189
|
-
bumpNonce !== providedNonce
|
|
83
|
+
assert(
|
|
84
|
+
to.toLowerCase() !== ARBITRARY_ADDRESS,
|
|
85
|
+
`The receiving wallet address must not be ${ARBITRARY_ADDRESS}`
|
|
190
86
|
)
|
|
191
|
-
throw new ErrorWrapper.EthLikeError({
|
|
192
|
-
message: new Error('incorrect nonce for replacement transaction'),
|
|
193
|
-
reason: ErrorWrapper.reasons.bumpTxFailed,
|
|
194
|
-
hint: 'providedNonce',
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
// Choose a nonce that compensates for transctions which are currently
|
|
198
|
-
// in pending; for example, when we send transactions at low gas which
|
|
199
|
-
// will be stored by `geth` for execution at a later point in time.
|
|
200
|
-
//
|
|
201
|
-
// When we are not intentionally bumping a transaction, users are
|
|
202
|
-
// appending a new transaction to the chain - therefore we should
|
|
203
|
-
// be mindful of nonces belonging to us which are currently pending.
|
|
204
|
-
const resolvedNonce =
|
|
205
|
-
providedNonce ??
|
|
206
|
-
bumpNonce ??
|
|
207
|
-
(await asset.baseAsset.getNonce({ asset, fromAddress, walletAccount }))
|
|
208
87
|
|
|
209
|
-
|
|
210
|
-
assetClientInterface,
|
|
88
|
+
let { txId, rawTx } = await signTx({
|
|
211
89
|
asset,
|
|
90
|
+
unsignedTx,
|
|
212
91
|
walletAccount,
|
|
213
|
-
|
|
214
|
-
amount,
|
|
215
|
-
nonce: resolvedNonce,
|
|
216
|
-
fromAddress,
|
|
217
|
-
customFee,
|
|
218
|
-
feeOpts,
|
|
219
|
-
txInput,
|
|
220
|
-
keepTxInput,
|
|
221
|
-
isSendAll,
|
|
222
|
-
createUnsignedTx,
|
|
223
|
-
feeData,
|
|
224
|
-
providedFeeAmount,
|
|
225
|
-
}
|
|
92
|
+
})
|
|
226
93
|
|
|
227
|
-
|
|
94
|
+
const isPrivate = Boolean(legacyParams?.options?.isPrivate)
|
|
228
95
|
|
|
229
96
|
if (isPrivate && typeof baseAsset.broadcastPrivateTx !== 'function')
|
|
230
97
|
throw new Error(
|
|
@@ -250,7 +117,7 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
|
|
|
250
117
|
reason: ErrorWrapper.reasons.insufficientFunds,
|
|
251
118
|
hint: 'broadcastTx',
|
|
252
119
|
})
|
|
253
|
-
} else if (bumpTxId) {
|
|
120
|
+
} else if (unsignedTx.txMeta.bumpTxId) {
|
|
254
121
|
throw new ErrorWrapper.EthLikeError({
|
|
255
122
|
message: err.message,
|
|
256
123
|
reason: ErrorWrapper.reasons.bumpTxFailed,
|
|
@@ -262,18 +129,23 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
|
|
|
262
129
|
reason: ErrorWrapper.reasons.broadcastTxFailed,
|
|
263
130
|
hint: 'otherErr:broadcastTx',
|
|
264
131
|
})
|
|
265
|
-
} else if (nonceTooLowErr && !isHardware) {
|
|
132
|
+
} else if (nonceTooLowErr && !unsignedTx.txMeta.isHardware) {
|
|
266
133
|
console.info('trying to send again...') // inject logger factory from platform
|
|
267
134
|
// let's try to fix the nonce issue
|
|
268
|
-
|
|
135
|
+
const newNonce = await asset.baseAsset.getNonce({
|
|
269
136
|
asset,
|
|
270
137
|
fromAddress,
|
|
271
|
-
|
|
272
|
-
txLog: baseAssetTxLog,
|
|
138
|
+
walletAccount,
|
|
273
139
|
triedNonce: nonce,
|
|
274
140
|
forceFromNode: true,
|
|
275
141
|
})
|
|
276
|
-
|
|
142
|
+
|
|
143
|
+
unsignedTx.txData.transactionBuffer = updateNonce(
|
|
144
|
+
unsignedTx.txData.transactionBuffer,
|
|
145
|
+
newNonce
|
|
146
|
+
)
|
|
147
|
+
nonce = newNonce
|
|
148
|
+
;({ txId, rawTx } = await signTx({ asset, unsignedTx, walletAccount }))
|
|
277
149
|
|
|
278
150
|
try {
|
|
279
151
|
await baseAsset.api.broadcastTx(rawTx.toString('hex'))
|
|
@@ -296,7 +168,18 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
|
|
|
296
168
|
}
|
|
297
169
|
}
|
|
298
170
|
|
|
299
|
-
const
|
|
171
|
+
const txData = eip1559Enabled
|
|
172
|
+
? {
|
|
173
|
+
gasLimit,
|
|
174
|
+
replacedTxId,
|
|
175
|
+
nonce,
|
|
176
|
+
...(tipGasPrice ? { tipGasPrice: tipGasPrice.toBaseString() } : Object.create(null)),
|
|
177
|
+
}
|
|
178
|
+
: {
|
|
179
|
+
gasLimit,
|
|
180
|
+
replacedTxId,
|
|
181
|
+
nonce,
|
|
182
|
+
}
|
|
300
183
|
|
|
301
184
|
await assetClientInterface.updateTxLogAndNotify({
|
|
302
185
|
assetName: asset.name,
|
|
@@ -310,21 +193,12 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
|
|
|
310
193
|
feeAmount,
|
|
311
194
|
feeCoinName: asset.feeAsset.name,
|
|
312
195
|
selfSend,
|
|
313
|
-
to
|
|
196
|
+
to,
|
|
314
197
|
currencies: {
|
|
315
198
|
[assetName]: asset.currency,
|
|
316
199
|
[asset.feeAsset.name]: asset.feeAsset.currency,
|
|
317
200
|
},
|
|
318
|
-
data:
|
|
319
|
-
? {
|
|
320
|
-
gasLimit,
|
|
321
|
-
replacedTxId: bumpTxId,
|
|
322
|
-
nonce,
|
|
323
|
-
...(tipGasPrice
|
|
324
|
-
? { tipGasPrice: tipGasPrice.toBaseString() }
|
|
325
|
-
: Object.create(null)),
|
|
326
|
-
}
|
|
327
|
-
: { gasLimit, replacedTxId: bumpTxId, nonce },
|
|
201
|
+
data: txData,
|
|
328
202
|
},
|
|
329
203
|
],
|
|
330
204
|
})
|
|
@@ -342,20 +216,13 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
|
|
|
342
216
|
feeAmount,
|
|
343
217
|
feeCoinName: baseAsset.name,
|
|
344
218
|
selfSend,
|
|
345
|
-
to
|
|
219
|
+
to,
|
|
346
220
|
token: asset.name,
|
|
347
221
|
currencies: {
|
|
348
222
|
[baseAsset.name]: baseAsset.currency,
|
|
349
223
|
[asset.feeAsset.name]: asset.feeAsset.currency,
|
|
350
224
|
},
|
|
351
|
-
data:
|
|
352
|
-
? {
|
|
353
|
-
gasLimit,
|
|
354
|
-
replacedTxId: bumpTxId,
|
|
355
|
-
nonce,
|
|
356
|
-
...(tipGasPrice ? { tipGasPrice: tipGasPrice.toBaseString() } : {}),
|
|
357
|
-
}
|
|
358
|
-
: { gasLimit, replacedTxId: bumpTxId, nonce },
|
|
225
|
+
data: txData,
|
|
359
226
|
},
|
|
360
227
|
],
|
|
361
228
|
})
|
|
@@ -365,146 +232,4 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx, useAbsoluteBala
|
|
|
365
232
|
}
|
|
366
233
|
}
|
|
367
234
|
|
|
368
|
-
const createTx = async ({
|
|
369
|
-
assetClientInterface,
|
|
370
|
-
asset,
|
|
371
|
-
walletAccount,
|
|
372
|
-
toAddress,
|
|
373
|
-
amount,
|
|
374
|
-
nonce,
|
|
375
|
-
txInput,
|
|
376
|
-
keepTxInput = false,
|
|
377
|
-
customFee,
|
|
378
|
-
isSendAll,
|
|
379
|
-
fromAddress,
|
|
380
|
-
feeOpts,
|
|
381
|
-
createUnsignedTx,
|
|
382
|
-
feeData,
|
|
383
|
-
providedFeeAmount,
|
|
384
|
-
}) => {
|
|
385
|
-
assert(
|
|
386
|
-
nonce !== undefined && typeof nonce === 'number',
|
|
387
|
-
'Nonce must be provided when creating a tx'
|
|
388
|
-
)
|
|
389
|
-
const isToken = isEthereumLikeToken(asset)
|
|
390
|
-
|
|
391
|
-
if (txInput && isToken && !keepTxInput)
|
|
392
|
-
throw new Error(`Additional data for Ethereum Token (${asset.name}) is not allowed`)
|
|
393
|
-
|
|
394
|
-
txInput =
|
|
395
|
-
isToken && !keepTxInput
|
|
396
|
-
? asset.contract.transfer.build(toAddress.toLowerCase(), amount.toBaseString())
|
|
397
|
-
: txInput
|
|
398
|
-
|
|
399
|
-
let { gasLimit, gasPrice, tipGasPrice, eip1559Enabled } = await getFeeInfo({
|
|
400
|
-
assetClientInterface,
|
|
401
|
-
asset,
|
|
402
|
-
fromAddress,
|
|
403
|
-
toAddress,
|
|
404
|
-
amount,
|
|
405
|
-
txInput,
|
|
406
|
-
feeOpts,
|
|
407
|
-
feeData,
|
|
408
|
-
customFee,
|
|
409
|
-
})
|
|
410
|
-
|
|
411
|
-
const isContractToAddress = await isContractAddressCached({ asset, address: toAddress })
|
|
412
|
-
|
|
413
|
-
// HACK: We cannot ensure the no dust invariant for `isSendAll`
|
|
414
|
-
// transactions to contract addresses, since we may be
|
|
415
|
-
// performing a raw token transaction and the parameter
|
|
416
|
-
// applies to the token and not the native amount.
|
|
417
|
-
//
|
|
418
|
-
// Contracts have nondeterministic gas most of the time
|
|
419
|
-
// versus estimations, anyway.
|
|
420
|
-
const isSendAllBaseAsset = isSendAll && !isToken && !isContractToAddress
|
|
421
|
-
|
|
422
|
-
// For native send all transactions, we have to make sure that
|
|
423
|
-
// the `tipGasPrice` is equal to the `gasPrice`, since this is
|
|
424
|
-
// effectively like saying that the `maxFeePerGas` is equal
|
|
425
|
-
// to the `maxPriorityFeePerGas`. We do this so that for a
|
|
426
|
-
// fixed gas cost transaction, no dust balance should remain,
|
|
427
|
-
// since any deviation in the underlying `baseFeePerGas` will
|
|
428
|
-
// result only affect the tip for the miner - no dust remains.
|
|
429
|
-
if (eip1559Enabled && isSendAllBaseAsset) {
|
|
430
|
-
// force consuming all gas
|
|
431
|
-
tipGasPrice = gasPrice
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// HACK: If we are handling a send all transaction, we must ensure
|
|
435
|
-
// the send all invariant is maintained before producing the
|
|
436
|
-
// final transaction.
|
|
437
|
-
const maybeOverrideSendAllAmount =
|
|
438
|
-
isSendAllBaseAsset &&
|
|
439
|
-
(await HACK_maybeRefineSendAllAmount({
|
|
440
|
-
amount,
|
|
441
|
-
asset,
|
|
442
|
-
assetClientInterface,
|
|
443
|
-
walletAccount,
|
|
444
|
-
gasLimit,
|
|
445
|
-
gasPrice,
|
|
446
|
-
}))
|
|
447
|
-
|
|
448
|
-
if (maybeOverrideSendAllAmount) {
|
|
449
|
-
console.log(
|
|
450
|
-
`Attempted to execute a sendAll transaction with an amount of ${amount.toDefaultString({ unit: true })}, but this would fail to maintain the no dust invariant! Overriding with ${maybeOverrideSendAllAmount.toDefaultString({ unit: true })}.`
|
|
451
|
-
)
|
|
452
|
-
amount = maybeOverrideSendAllAmount
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const unsignedTx = await createUnsignedTx({
|
|
456
|
-
asset,
|
|
457
|
-
walletAccount,
|
|
458
|
-
address: toAddress,
|
|
459
|
-
amount,
|
|
460
|
-
nonce,
|
|
461
|
-
txInput,
|
|
462
|
-
gasLimit,
|
|
463
|
-
gasPrice,
|
|
464
|
-
tipGasPrice,
|
|
465
|
-
fromAddress,
|
|
466
|
-
eip1559Enabled,
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
// TODO: move into createUnsignedTx()
|
|
470
|
-
if (keepTxInput && !isToken) {
|
|
471
|
-
unsignedTx.txData.to = toAddress
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
unsignedTx.txMeta.eip1559Enabled = eip1559Enabled
|
|
475
|
-
|
|
476
|
-
const resolveFee = async () => {
|
|
477
|
-
if (providedFeeAmount) {
|
|
478
|
-
return providedFeeAmount
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
const optimismL1DataFee = asset.baseAsset.estimateL1DataFee
|
|
482
|
-
? await asset.baseAsset.estimateL1DataFee({
|
|
483
|
-
unsignedTx,
|
|
484
|
-
})
|
|
485
|
-
: undefined
|
|
486
|
-
|
|
487
|
-
const l1DataFee = optimismL1DataFee
|
|
488
|
-
? asset.baseAsset.currency.baseUnit(optimismL1DataFee)
|
|
489
|
-
: asset.baseAsset.currency.ZERO
|
|
490
|
-
return gasPrice.mul(gasLimit).add(l1DataFee)
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const { txId, rawTx } = await assetClientInterface.signTransaction({
|
|
494
|
-
assetName: asset.baseAsset.name,
|
|
495
|
-
unsignedTx,
|
|
496
|
-
walletAccount,
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
return {
|
|
500
|
-
txId: normalizeTxId(txId),
|
|
501
|
-
rawTx,
|
|
502
|
-
nonce,
|
|
503
|
-
gasLimit,
|
|
504
|
-
gasPrice,
|
|
505
|
-
tipGasPrice,
|
|
506
|
-
feeAmount: await resolveFee(),
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
235
|
export default txSendFactory
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { ensureSaneEip1559GasPriceForTipGasPrice } from '../fee-utils.js'
|
|
2
|
-
import { fetchGasLimit } from '../gas-estimation.js'
|
|
3
|
-
import { getFeeFactoryGasPrices } from '../get-fee.js'
|
|
4
|
-
|
|
5
|
-
const getFeeInfo = async function getFeeInfo({
|
|
6
|
-
assetClientInterface,
|
|
7
|
-
asset,
|
|
8
|
-
fromAddress,
|
|
9
|
-
toAddress,
|
|
10
|
-
amount,
|
|
11
|
-
txInput,
|
|
12
|
-
feeOpts = {},
|
|
13
|
-
feeData,
|
|
14
|
-
customFee,
|
|
15
|
-
}) {
|
|
16
|
-
// HACK: Previously, calls `getFeeInfo` were not provided a reference
|
|
17
|
-
// to `feeData`. For backwards compatibility, we'll revert to
|
|
18
|
-
// legacy behaviour.
|
|
19
|
-
// NOTE: This shouldn't actually be used outside of the `assets` repo;
|
|
20
|
-
// this is done just for safety.
|
|
21
|
-
if (!feeData) {
|
|
22
|
-
console.warn('`getFeeInfo` was not explicitly passed a `feeData` object.')
|
|
23
|
-
const { name: assetName } = asset
|
|
24
|
-
feeData = await assetClientInterface.getFeeData({ assetName })
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const {
|
|
28
|
-
gasPrice: gasPrice_,
|
|
29
|
-
feeData: { tipGasPrice: tipGasPrice_, eip1559Enabled },
|
|
30
|
-
} = getFeeFactoryGasPrices({ customFee, feeData })
|
|
31
|
-
|
|
32
|
-
const tipGasPrice = feeOpts.tipGasPrice || tipGasPrice_
|
|
33
|
-
|
|
34
|
-
const maybeGasPrice = feeOpts.gasPrice || gasPrice_
|
|
35
|
-
|
|
36
|
-
// HACK: If we've received an invalid combination of `tipGasPrice`
|
|
37
|
-
// (maxPriorityFeePerGas) and `gasPrice` (maxFeePerGas), then
|
|
38
|
-
// we must normalize these before returning.
|
|
39
|
-
const gasPrice = eip1559Enabled
|
|
40
|
-
? ensureSaneEip1559GasPriceForTipGasPrice({ gasPrice: maybeGasPrice, tipGasPrice })
|
|
41
|
-
: maybeGasPrice
|
|
42
|
-
|
|
43
|
-
const gasLimit =
|
|
44
|
-
feeOpts.gasLimit ||
|
|
45
|
-
(await fetchGasLimit({
|
|
46
|
-
asset,
|
|
47
|
-
fromAddress,
|
|
48
|
-
toAddress,
|
|
49
|
-
amount,
|
|
50
|
-
txInput,
|
|
51
|
-
feeData,
|
|
52
|
-
throwOnError: false,
|
|
53
|
-
}))
|
|
54
|
-
|
|
55
|
-
return { gasPrice, gasLimit, tipGasPrice, eip1559Enabled }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export default getFeeInfo
|