@exodus/ethereum-api 8.53.5 → 8.54.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 +22 -0
- package/package.json +3 -3
- package/src/gas-estimation.js +61 -47
- package/src/index.js +0 -1
- package/src/nft-utils.js +1 -2
- package/src/tx-create.js +178 -138
- package/src/tx-log/get-optimistic-txlog-effects.js +7 -5
- package/src/tx-send/tx-send.js +3 -4
- package/src/tx-type/index.js +261 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
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.54.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.53.6...@exodus/ethereum-api@8.54.0) (2025-10-23)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat: enable contract deployment via evm asset api (#6742)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [8.53.6](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.53.5...@exodus/ethereum-api@8.53.6) (2025-10-23)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* fix: improve transaction address resolution for gas estimation and encapsulate transaction property evaluation (#6636)
|
|
23
|
+
|
|
24
|
+
* fix: increase evm resistance to txLog race conditions (#6676)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
## [8.53.5](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.53.4...@exodus/ethereum-api@8.53.5) (2025-10-17)
|
|
7
29
|
|
|
8
30
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.54.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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@exodus/bip44-constants": "^195.0.0",
|
|
30
30
|
"@exodus/crypto": "^1.0.0-rc.26",
|
|
31
31
|
"@exodus/currency": "^6.0.1",
|
|
32
|
-
"@exodus/ethereum-lib": "^5.18.
|
|
32
|
+
"@exodus/ethereum-lib": "^5.18.2",
|
|
33
33
|
"@exodus/ethereum-meta": "^2.9.1",
|
|
34
34
|
"@exodus/ethereumholesky-meta": "^2.0.5",
|
|
35
35
|
"@exodus/ethereumjs": "^1.8.0",
|
|
@@ -67,5 +67,5 @@
|
|
|
67
67
|
"type": "git",
|
|
68
68
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
69
69
|
},
|
|
70
|
-
"gitHead": "
|
|
70
|
+
"gitHead": "bcb81073e271d505ae3f11c60a6d5e1857f67a28"
|
|
71
71
|
}
|
package/src/gas-estimation.js
CHANGED
|
@@ -2,6 +2,11 @@ import { currency2buffer, isEthereumLikeToken } from '@exodus/ethereum-lib'
|
|
|
2
2
|
import { bufferToHex, toBuffer } from '@exodus/ethereumjs/util'
|
|
3
3
|
|
|
4
4
|
import { estimateGas, isContractAddressCached, isForwarderContractCached } from './eth-like-util.js'
|
|
5
|
+
import {
|
|
6
|
+
ARBITRARY_ADDRESS,
|
|
7
|
+
resolveCriticalTxAttributes,
|
|
8
|
+
TX_TYPE_TRANSFER,
|
|
9
|
+
} from './tx-type/index.js'
|
|
5
10
|
|
|
6
11
|
export const DEFAULT_GAS_LIMIT_MULTIPLIER = 1.29
|
|
7
12
|
|
|
@@ -12,23 +17,6 @@ export const DEFAULT_CONTRACT_GAS_LIMIT = 1e6
|
|
|
12
17
|
export const DEFAULT_TOKEN_GAS_LIMIT = 120e3
|
|
13
18
|
export const DEFAULT_GAS_LIMIT = 21_000
|
|
14
19
|
|
|
15
|
-
// HACK: If a recipient address is not defined, we usually fall back to
|
|
16
|
-
// default address so gas estimation can still complete successfully
|
|
17
|
-
// without knowledge of which accounts are involved.
|
|
18
|
-
//
|
|
19
|
-
// However, we must be careful to select addresses which are unlikely
|
|
20
|
-
// to have existing obligations, such as popular dead addresses or the
|
|
21
|
-
// reserved addresses of precompiles, since these can influence gas
|
|
22
|
-
// estimation.
|
|
23
|
-
//
|
|
24
|
-
// Here, we use an address which is mostly all `1`s to make sure we can
|
|
25
|
-
// exaggerate the worst-case calldata cost (which is priced per high bit)
|
|
26
|
-
// whilst being unlikely to have any token balances.
|
|
27
|
-
//
|
|
28
|
-
// Unfortunately, we can't use `0xffffffffffffffffffffffffffffffffffffffff`,
|
|
29
|
-
// since this address is a whale.
|
|
30
|
-
export const ARBITRARY_ADDRESS = '0xfffFfFfFfFfFFFFFfeFfFFFffFffFFFFfFFFFFFF'.toLowerCase()
|
|
31
|
-
|
|
32
20
|
// HACK: RPCs generally provide imprecise estimates
|
|
33
21
|
// for `gasUsed` (often these are insufficient).
|
|
34
22
|
export const scaleGasLimitEstimate = ({
|
|
@@ -56,6 +44,7 @@ export async function estimateGasLimit({
|
|
|
56
44
|
asset,
|
|
57
45
|
fromAddress,
|
|
58
46
|
toAddress,
|
|
47
|
+
// TODO: rename to value, as this is `msg.value`.
|
|
59
48
|
amount = asset.currency.ZERO,
|
|
60
49
|
data,
|
|
61
50
|
gasPrice = '0x',
|
|
@@ -76,7 +65,13 @@ export async function resolveGasLimitMultiplier({ asset, feeData, toAddress, fro
|
|
|
76
65
|
const gasLimitMultiplierWhenUnknownAddress =
|
|
77
66
|
feeData?.gasLimits?.[asset.name]?.gasLimitMultiplierWhenUnknownAddress
|
|
78
67
|
|
|
79
|
-
|
|
68
|
+
// NOTE: If either the `fromAddress` or `toAddress` has fell
|
|
69
|
+
// back to the `ARBITRARY_ADDRESS`, these qualify as
|
|
70
|
+
// "unknown" addresses.
|
|
71
|
+
const fromAddressIsUnknown = fromAddress === ARBITRARY_ADDRESS
|
|
72
|
+
const toAddressIsUnknown = toAddress === ARBITRARY_ADDRESS
|
|
73
|
+
|
|
74
|
+
if (gasLimitMultiplierWhenUnknownAddress && (fromAddressIsUnknown || toAddressIsUnknown)) {
|
|
80
75
|
return gasLimitMultiplierWhenUnknownAddress
|
|
81
76
|
}
|
|
82
77
|
|
|
@@ -103,12 +98,6 @@ export async function resolveGasLimitMultiplier({ asset, feeData, toAddress, fro
|
|
|
103
98
|
return DEFAULT_GAS_LIMIT_MULTIPLIER
|
|
104
99
|
}
|
|
105
100
|
|
|
106
|
-
export function resolveDefaultTxInput({ asset, toAddress, amount }) {
|
|
107
|
-
return isEthereumLikeToken(asset)
|
|
108
|
-
? bufferToHex(asset.contract.transfer.build(toAddress.toLowerCase(), amount?.toBaseString()))
|
|
109
|
-
: '0x'
|
|
110
|
-
}
|
|
111
|
-
|
|
112
101
|
export const defaultGasLimit = ({ asset, txInput }) => {
|
|
113
102
|
const isToken = isEthereumLikeToken(asset)
|
|
114
103
|
return (
|
|
@@ -117,6 +106,7 @@ export const defaultGasLimit = ({ asset, txInput }) => {
|
|
|
117
106
|
)
|
|
118
107
|
}
|
|
119
108
|
|
|
109
|
+
// TODO: `gasLimit` needs to be a responsibility of `resolveTxAttributesByTxType`.
|
|
120
110
|
export async function fetchGasLimit({
|
|
121
111
|
asset,
|
|
122
112
|
feeData,
|
|
@@ -124,8 +114,9 @@ export async function fetchGasLimit({
|
|
|
124
114
|
toAddress: providedToAddress,
|
|
125
115
|
txInput: providedTxInput,
|
|
126
116
|
amount: providedAmount,
|
|
127
|
-
contractAddress,
|
|
117
|
+
contractAddress: providedTxToAddress,
|
|
128
118
|
bip70,
|
|
119
|
+
txType = TX_TYPE_TRANSFER,
|
|
129
120
|
throwOnError = true,
|
|
130
121
|
}) {
|
|
131
122
|
if (bip70?.bitpay?.data && bip70?.bitpay?.gasPrice) {
|
|
@@ -133,16 +124,31 @@ export async function fetchGasLimit({
|
|
|
133
124
|
return asset.name === 'ethereum' ? 65_000 : 130_000
|
|
134
125
|
}
|
|
135
126
|
|
|
136
|
-
|
|
127
|
+
// TODO: If we can provide the `walletAccount` via `fetchGasLimit` then
|
|
128
|
+
// we can use `resolveTxAttributesByTxType` instead (and remove
|
|
129
|
+
// the concept of critical attributes).
|
|
130
|
+
const { txToAddress, txInput, txValue } = resolveCriticalTxAttributes({
|
|
131
|
+
asset,
|
|
132
|
+
amount: providedAmount,
|
|
133
|
+
toAddress: providedToAddress,
|
|
134
|
+
txToAddress: providedTxToAddress,
|
|
135
|
+
txInput: providedTxInput,
|
|
136
|
+
txType,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const isContractTxToAddress =
|
|
140
|
+
Boolean(txToAddress) && (await isContractAddressCached({ asset, address: txToAddress }))
|
|
141
|
+
|
|
137
142
|
const fromAddress = providedFromAddress ?? ARBITRARY_ADDRESS
|
|
138
|
-
const toAddress = providedToAddress ?? ARBITRARY_ADDRESS
|
|
139
|
-
const txInput = providedTxInput || resolveDefaultTxInput({ asset, toAddress, amount })
|
|
140
143
|
|
|
141
144
|
const isToken = isEthereumLikeToken(asset)
|
|
142
145
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
if (
|
|
147
|
+
txType === TX_TYPE_TRANSFER &&
|
|
148
|
+
!isToken &&
|
|
149
|
+
!isContractTxToAddress &&
|
|
150
|
+
!asset.forceGasLimitEstimation
|
|
151
|
+
) {
|
|
146
152
|
return defaultGasLimit({ asset, txInput })
|
|
147
153
|
}
|
|
148
154
|
|
|
@@ -150,39 +156,47 @@ export async function fetchGasLimit({
|
|
|
150
156
|
asset,
|
|
151
157
|
feeData,
|
|
152
158
|
fromAddress: providedFromAddress,
|
|
153
|
-
toAddress:
|
|
159
|
+
toAddress: txToAddress,
|
|
154
160
|
})
|
|
155
161
|
|
|
156
|
-
const txToAddress = contractAddress ?? (isToken ? asset.contract.address : toAddress)
|
|
157
|
-
const txAmount = isToken ? asset.baseAsset.currency.ZERO : amount
|
|
158
|
-
|
|
159
162
|
try {
|
|
163
|
+
// NOTE: Although we'll return the `fixedGasLimit` for known
|
|
164
|
+
// tokens, we'll still want to execute `estimateGasLimit`
|
|
165
|
+
// to verify their transaction actually succeeds (i.e.
|
|
166
|
+
// they aren't trying to transfer more than their balance).
|
|
167
|
+
//
|
|
168
|
+
// This prevents users from submitting `fixedGasLimit`
|
|
169
|
+
// token transactions which result in a `revert`.
|
|
160
170
|
const estimatedGasLimit = await estimateGasLimit({
|
|
161
171
|
asset,
|
|
162
172
|
fromAddress,
|
|
163
173
|
toAddress: txToAddress,
|
|
164
|
-
amount:
|
|
174
|
+
amount: txValue,
|
|
165
175
|
data: txInput,
|
|
166
176
|
})
|
|
167
177
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
//
|
|
178
|
+
const scaledGasLimitEstimate = scaleGasLimitEstimate({ estimatedGasLimit, gasLimitMultiplier })
|
|
179
|
+
if (!isToken) return scaledGasLimitEstimate
|
|
180
|
+
|
|
181
|
+
// NOTE: If we've enabled `fixGasLimit`s for a token,
|
|
182
|
+
// we need to make sure that transaction we're
|
|
183
|
+
// making actually targets that token.
|
|
172
184
|
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
)
|
|
185
|
+
// Otherwise, we risk applying the fixed gas limit
|
|
186
|
+
// to transactions with _other_ smart contracts
|
|
187
|
+
// simply because the caller specified the token
|
|
188
|
+
// asset.
|
|
189
|
+
const isTokenTransaction = asset.contract.address.toLowerCase() === txToAddress.toLowerCase()
|
|
190
|
+
if (!isTokenTransaction) return scaledGasLimitEstimate
|
|
191
|
+
|
|
192
|
+
return feeData?.gasLimits?.[asset.name]?.fixedGasLimit ?? scaledGasLimitEstimate
|
|
179
193
|
} catch (err) {
|
|
180
194
|
if (throwOnError) throw err
|
|
181
195
|
|
|
182
196
|
console.error('fetchGasLimit error', err)
|
|
183
197
|
|
|
184
198
|
// fallback value for contract case
|
|
185
|
-
if (
|
|
199
|
+
if (isContractTxToAddress) return DEFAULT_CONTRACT_GAS_LIMIT
|
|
186
200
|
|
|
187
201
|
return defaultGasLimit({ asset, txInput })
|
|
188
202
|
}
|
package/src/index.js
CHANGED
package/src/nft-utils.js
CHANGED
|
@@ -34,9 +34,8 @@ export const getNftArguments = async ({ asset, nft, fromAddress, toAddress }) =>
|
|
|
34
34
|
const gasLimit = await fetchGasLimit({
|
|
35
35
|
asset,
|
|
36
36
|
fromAddress,
|
|
37
|
-
|
|
37
|
+
contractAddress,
|
|
38
38
|
txInput,
|
|
39
|
-
amount: asset.baseAsset.currency.ZERO,
|
|
40
39
|
})
|
|
41
40
|
return { txInput, gasLimit }
|
|
42
41
|
} catch (e) {
|
package/src/tx-create.js
CHANGED
|
@@ -2,51 +2,65 @@ import { calculateBumpedGasPrice, currency2buffer, isEthereumLikeToken } from '@
|
|
|
2
2
|
import createEthereumJsTx from '@exodus/ethereum-lib/src/unsigned-tx/create-ethereumjs-tx.js'
|
|
3
3
|
import assert from 'minimalistic-assert'
|
|
4
4
|
|
|
5
|
-
import * as ErrorWrapper from './error-wrapper.js'
|
|
6
|
-
import { isContractAddressCached } from './eth-like-util.js'
|
|
7
5
|
import { ensureSaneEip1559GasPriceForTipGasPrice } from './fee-utils.js'
|
|
8
|
-
import {
|
|
6
|
+
import { fetchGasLimit } from './gas-estimation.js'
|
|
9
7
|
import { getExtraFeeData, getFeeFactoryGasPrices } from './get-fee.js'
|
|
10
8
|
import { getNftArguments } from './nft-utils.js'
|
|
11
9
|
import { getHighestIncentivePendingTxByNonce } from './tx-log/index.js'
|
|
10
|
+
import {
|
|
11
|
+
assertTxAttributes,
|
|
12
|
+
isValidTxType,
|
|
13
|
+
resolveTxAttributesByTxType,
|
|
14
|
+
resolveTxFromAddress,
|
|
15
|
+
TX_TYPE_TRANSFER,
|
|
16
|
+
} from './tx-type/index.js'
|
|
12
17
|
|
|
13
18
|
async function createUnsignedTxWithFees({
|
|
14
19
|
asset,
|
|
15
20
|
chainId,
|
|
16
|
-
to, // the tx to address, it could be the reciver address for native sending, the token contract, the DEX contract, etc
|
|
17
|
-
value, // the value of the tx in NU, it can be the value in eth or 0 when calling contracts
|
|
18
|
-
data, // the data of the tx in hex string, it can be 0x for native sending or the params when sending to a contract
|
|
19
21
|
gasLimit,
|
|
20
22
|
eip1559Enabled,
|
|
21
23
|
gasPrice, // eip 1559: `maxFeePerGas`
|
|
22
24
|
tipGasPrice, // eip 1559: `maxPriorityPerGas`
|
|
23
|
-
nonce,
|
|
24
25
|
bumpTxId,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
toAddress,
|
|
26
|
+
/* txAttributes */
|
|
27
|
+
amount,
|
|
28
|
+
toAddress,
|
|
29
|
+
txInput,
|
|
30
|
+
txToAddress,
|
|
31
|
+
txType,
|
|
32
|
+
txValue,
|
|
33
|
+
fromAddress,
|
|
34
|
+
nonce,
|
|
35
|
+
isContractTxToAddress,
|
|
28
36
|
}) {
|
|
29
37
|
assert(asset, 'asset is required')
|
|
30
38
|
assert(typeof chainId === 'number', 'chainId is required')
|
|
31
|
-
assert(to, 'to is required')
|
|
32
|
-
assert(value, 'value is required')
|
|
33
|
-
assert(data, 'data is required')
|
|
34
39
|
assert(gasLimit, 'gasLimit is required')
|
|
35
40
|
assert(gasPrice, 'gasPrice is required')
|
|
36
|
-
assert(coinAmount, 'coinAmount is required')
|
|
37
|
-
assert(fromAddress, 'fromAddress is required')
|
|
38
|
-
assert(toAddress, 'toAddress is required')
|
|
39
41
|
assert(typeof eip1559Enabled === 'boolean', 'eip1559Enabled is required')
|
|
40
42
|
|
|
43
|
+
assertTxAttributes({
|
|
44
|
+
amount,
|
|
45
|
+
toAddress,
|
|
46
|
+
txInput,
|
|
47
|
+
txToAddress,
|
|
48
|
+
txType,
|
|
49
|
+
txValue,
|
|
50
|
+
fromAddress,
|
|
51
|
+
nonce,
|
|
52
|
+
isContractTxToAddress,
|
|
53
|
+
})
|
|
54
|
+
|
|
41
55
|
const ethjsTx = createEthereumJsTx({
|
|
42
56
|
txData: {
|
|
43
57
|
nonce,
|
|
44
58
|
gasPrice: currency2buffer(gasPrice),
|
|
45
59
|
tipGasPrice: tipGasPrice ? currency2buffer(tipGasPrice) : undefined,
|
|
46
60
|
gasLimit,
|
|
47
|
-
to,
|
|
48
|
-
value: currency2buffer(
|
|
49
|
-
data,
|
|
61
|
+
to: txToAddress,
|
|
62
|
+
value: currency2buffer(txValue),
|
|
63
|
+
data: txInput,
|
|
50
64
|
chainId,
|
|
51
65
|
},
|
|
52
66
|
txMeta: {
|
|
@@ -67,7 +81,7 @@ async function createUnsignedTxWithFees({
|
|
|
67
81
|
: asset.baseAsset.currency.ZERO
|
|
68
82
|
|
|
69
83
|
const fee = baseFee.add(l1DataFee)
|
|
70
|
-
const extraFeeData = getExtraFeeData({ asset, amount
|
|
84
|
+
const extraFeeData = getExtraFeeData({ asset, amount })
|
|
71
85
|
const unsignedTx = {
|
|
72
86
|
txData: { transactionBuffer, chainId },
|
|
73
87
|
txMeta: {
|
|
@@ -76,7 +90,7 @@ async function createUnsignedTxWithFees({
|
|
|
76
90
|
eip1559Enabled,
|
|
77
91
|
fromAddress,
|
|
78
92
|
toAddress,
|
|
79
|
-
amount:
|
|
93
|
+
amount: amount.toDefaultString({ unit: true }),
|
|
80
94
|
fee: fee.toDefaultString({ unit: true }),
|
|
81
95
|
},
|
|
82
96
|
}
|
|
@@ -91,6 +105,35 @@ async function createUnsignedTxWithFees({
|
|
|
91
105
|
}
|
|
92
106
|
}
|
|
93
107
|
|
|
108
|
+
const resolveTxFactoryGasPrices = ({
|
|
109
|
+
customFee: providedCustomFee,
|
|
110
|
+
feeData,
|
|
111
|
+
gasPrice: providedGasPrice,
|
|
112
|
+
tipGasPrice: providedTipGasPrice,
|
|
113
|
+
}) => {
|
|
114
|
+
assert(feeData)
|
|
115
|
+
|
|
116
|
+
const {
|
|
117
|
+
gasPrice: maybeGasPrice,
|
|
118
|
+
feeData: { tipGasPrice: maybeTipGasPrice, eip1559Enabled },
|
|
119
|
+
} = getFeeFactoryGasPrices({ customFee: providedCustomFee, feeData })
|
|
120
|
+
|
|
121
|
+
const resolvedGasPrice = providedGasPrice ?? maybeGasPrice
|
|
122
|
+
|
|
123
|
+
if (!eip1559Enabled) return { eip1559Enabled, resolvedGasPrice }
|
|
124
|
+
|
|
125
|
+
const resolvedTipGasPrice = providedTipGasPrice ?? maybeTipGasPrice
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
eip1559Enabled,
|
|
129
|
+
resolvedGasPrice: ensureSaneEip1559GasPriceForTipGasPrice({
|
|
130
|
+
gasPrice: resolvedGasPrice,
|
|
131
|
+
tipGasPrice: resolvedTipGasPrice,
|
|
132
|
+
}),
|
|
133
|
+
resolvedTipGasPrice,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
94
137
|
const createBumpUnsignedTx = async ({
|
|
95
138
|
fromAddress,
|
|
96
139
|
chainId,
|
|
@@ -100,8 +143,11 @@ const createBumpUnsignedTx = async ({
|
|
|
100
143
|
assetClientInterface,
|
|
101
144
|
walletAccount,
|
|
102
145
|
feeData,
|
|
103
|
-
nonce:
|
|
146
|
+
nonce: maybeProvidedNonce,
|
|
147
|
+
txType,
|
|
104
148
|
}) => {
|
|
149
|
+
assert(isValidTxType(txType), 'invalid txType')
|
|
150
|
+
|
|
105
151
|
const baseAsset = asset.baseAsset
|
|
106
152
|
const replacedTx = baseAssetTxLog.get(bumpTxId)
|
|
107
153
|
const assets = await assetClientInterface.getAssetsForNetwork({ baseAssetName: baseAsset.name })
|
|
@@ -131,13 +177,40 @@ const createBumpUnsignedTx = async ({
|
|
|
131
177
|
}
|
|
132
178
|
|
|
133
179
|
const toAddress = (replacedTokenTx || replacedTx).to
|
|
134
|
-
const isToken = isEthereumLikeToken(asset)
|
|
135
|
-
const txToAddress = isToken ? asset.contract.address : toAddress
|
|
136
|
-
const coinAmount = (replacedTokenTx || replacedTx).coinAmount.negate()
|
|
137
|
-
const gasLimit = replacedTx.data.gasLimit
|
|
138
|
-
const nonce = replacedTx.data.nonce
|
|
139
180
|
|
|
140
|
-
const
|
|
181
|
+
const amount = (replacedTokenTx || replacedTx).coinAmount.negate()
|
|
182
|
+
|
|
183
|
+
const txInput = replacedTokenTx ? null : replacedTx.data.data || '0x'
|
|
184
|
+
|
|
185
|
+
const replacedTxNonce = replacedTx.data.nonce
|
|
186
|
+
|
|
187
|
+
assert(
|
|
188
|
+
Number.isInteger(replacedTxNonce),
|
|
189
|
+
`Cannot bump transaction ${bumpTxId}: data object seems to be corrupted`
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
// If we have evaluated a bump transaction and the `providedNonce` differs
|
|
193
|
+
// from the `bumpNonce`, we've encountered a conflict and cannot respect
|
|
194
|
+
// the caller's request.
|
|
195
|
+
if (maybeProvidedNonce && maybeProvidedNonce !== replacedTxNonce) {
|
|
196
|
+
throw new Error('incorrect nonce for replacement transaction')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const nonce = maybeProvidedNonce ?? replacedTxNonce
|
|
200
|
+
|
|
201
|
+
const resolvedTxAttributes = await resolveTxAttributesByTxType({
|
|
202
|
+
asset,
|
|
203
|
+
assetClientInterface,
|
|
204
|
+
fromAddress,
|
|
205
|
+
amount,
|
|
206
|
+
nonce,
|
|
207
|
+
txInput,
|
|
208
|
+
toAddress,
|
|
209
|
+
txType,
|
|
210
|
+
walletAccount,
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const gasLimit = replacedTx.data.gasLimit
|
|
141
214
|
|
|
142
215
|
const {
|
|
143
216
|
gasPrice: currentGasPrice,
|
|
@@ -149,13 +222,13 @@ const createBumpUnsignedTx = async ({
|
|
|
149
222
|
const maybeHighestIncentivePendingTxForNonce = await getHighestIncentivePendingTxByNonce({
|
|
150
223
|
assetClientInterface,
|
|
151
224
|
asset,
|
|
152
|
-
nonce,
|
|
225
|
+
nonce: resolvedTxAttributes.nonce,
|
|
153
226
|
walletAccount,
|
|
154
227
|
})
|
|
155
228
|
|
|
156
229
|
assert(
|
|
157
230
|
maybeHighestIncentivePendingTxForNonce,
|
|
158
|
-
`unable to resolve pending transaction for nonce ${nonce}`
|
|
231
|
+
`unable to resolve pending transaction for nonce ${resolvedTxAttributes.nonce}`
|
|
159
232
|
)
|
|
160
233
|
|
|
161
234
|
const { bumpedGasPrice, bumpedTipGasPrice } = calculateBumpedGasPrice({
|
|
@@ -171,41 +244,22 @@ const createBumpUnsignedTx = async ({
|
|
|
171
244
|
currentTipGasPrice,
|
|
172
245
|
eip1559Enabled,
|
|
173
246
|
})
|
|
174
|
-
const gasPrice = bumpedGasPrice
|
|
175
|
-
const tipGasPrice = bumpedTipGasPrice
|
|
176
|
-
const data = replacedTokenTx
|
|
177
|
-
? asset.contract.transfer.build(toAddress.toLowerCase(), coinAmount.toBaseString())
|
|
178
|
-
: replacedTx.data.data || '0x'
|
|
179
|
-
|
|
180
|
-
if (nonce === undefined) {
|
|
181
|
-
throw new Error(`Cannot bump transaction ${bumpTxId}: data object seems to be corrupted`)
|
|
182
|
-
}
|
|
183
247
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
message: new Error('incorrect nonce for replacement transaction'),
|
|
190
|
-
reason: ErrorWrapper.reasons.bumpTxFailed,
|
|
191
|
-
hint: 'providedNonce',
|
|
248
|
+
const { resolvedGasPrice: gasPrice, resolvedTipGasPrice: tipGasPrice } =
|
|
249
|
+
resolveTxFactoryGasPrices({
|
|
250
|
+
feeData,
|
|
251
|
+
gasPrice: bumpedGasPrice,
|
|
252
|
+
tipGasPrice: bumpedTipGasPrice,
|
|
192
253
|
})
|
|
193
|
-
}
|
|
194
254
|
|
|
195
255
|
return createUnsignedTxWithFees({
|
|
256
|
+
...resolvedTxAttributes,
|
|
196
257
|
asset,
|
|
197
258
|
chainId,
|
|
198
|
-
to: txToAddress,
|
|
199
|
-
value,
|
|
200
|
-
data,
|
|
201
259
|
gasLimit,
|
|
202
260
|
gasPrice,
|
|
203
261
|
tipGasPrice,
|
|
204
|
-
nonce,
|
|
205
262
|
bumpTxId,
|
|
206
|
-
coinAmount,
|
|
207
|
-
fromAddress,
|
|
208
|
-
toAddress,
|
|
209
263
|
eip1559Enabled,
|
|
210
264
|
})
|
|
211
265
|
}
|
|
@@ -220,33 +274,35 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
220
274
|
fromAddress: providedFromAddress, // wallet from address
|
|
221
275
|
toAddress: providedToAddress, // user's to address, not the token or the dex contract
|
|
222
276
|
txInput: providedTxInput, // Provided when swapping via a DEX contract
|
|
277
|
+
txType = TX_TYPE_TRANSFER, // Defines what kind of transaction is being performed.
|
|
223
278
|
gasLimit: providedGasLimit, // Provided by exchange when known
|
|
224
279
|
amount: providedAmount, // The NU amount to be sent, to be included in the tx value or tx input
|
|
225
280
|
nonce: providedNonce,
|
|
226
281
|
tipGasPrice: providedTipGasPrice,
|
|
227
282
|
gasPrice: providedGasPrice,
|
|
228
283
|
bip70,
|
|
229
|
-
customFee,
|
|
284
|
+
customFee: providedCustomFee,
|
|
230
285
|
isSendAll,
|
|
231
286
|
bumpTxId,
|
|
232
287
|
}) => {
|
|
233
288
|
assert(asset, 'asset is required')
|
|
234
289
|
assert(walletAccount, 'walletAccount is required')
|
|
290
|
+
assert(isValidTxType(txType), 'invalid txType')
|
|
235
291
|
|
|
236
292
|
const feeData = await assetClientInterface.getFeeConfig({ assetName: asset.baseAsset.name })
|
|
237
293
|
|
|
238
|
-
const fromAddress =
|
|
239
|
-
providedFromAddress ??
|
|
240
|
-
(await assetClientInterface.getReceiveAddress({
|
|
241
|
-
assetName: asset.baseAsset.name,
|
|
242
|
-
walletAccount,
|
|
243
|
-
}))
|
|
244
|
-
|
|
245
294
|
const baseAssetTxLog = await assetClientInterface.getTxLog({
|
|
246
295
|
assetName: asset.baseAsset.name,
|
|
247
296
|
walletAccount,
|
|
248
297
|
})
|
|
249
298
|
|
|
299
|
+
const fromAddress = await resolveTxFromAddress({
|
|
300
|
+
asset,
|
|
301
|
+
assetClientInterface,
|
|
302
|
+
fromAddress: providedFromAddress,
|
|
303
|
+
walletAccount,
|
|
304
|
+
})
|
|
305
|
+
|
|
250
306
|
if (bumpTxId) {
|
|
251
307
|
return createBumpUnsignedTx({
|
|
252
308
|
chainId,
|
|
@@ -258,62 +314,20 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
258
314
|
walletAccount,
|
|
259
315
|
feeData,
|
|
260
316
|
nonce: providedNonce,
|
|
317
|
+
txType,
|
|
261
318
|
})
|
|
262
319
|
}
|
|
263
320
|
|
|
264
|
-
const toAddress = providedToAddress || ARBITRARY_ADDRESS
|
|
265
321
|
const {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
// When sending a token, the tx 'to' address is the asset.contract?.address. No tx input is provided; it's resolved locally.
|
|
276
|
-
// When DEX swapping a main asset, the exchange provides a txInput and a DEX address. Use the DEX address as the tx 'to' address.
|
|
277
|
-
// When DEX swapping a token, the exchange provides a txInput and a DEX address. Use the DEX address as the tx 'to' address.
|
|
278
|
-
// When CEX swapping a main asset, the exchange may provide a txInput and an address. Use this address as the tx 'to' address.
|
|
279
|
-
// When CEX swapping a token, the exchange may provide a txInput but not an address. In this case, the tx 'to' address is the token address.
|
|
280
|
-
|
|
281
|
-
const txToAddress =
|
|
282
|
-
isToken && !providedTxInput
|
|
283
|
-
? asset.contract.address
|
|
284
|
-
: providedToAddress || asset.contract?.address || ARBITRARY_ADDRESS
|
|
285
|
-
|
|
286
|
-
const isContractToAddress = await isContractAddressCached({ asset, address: txToAddress })
|
|
287
|
-
|
|
288
|
-
// HACK: We cannot ensure the no dust invariant for `isSendAll`
|
|
289
|
-
// transactions to contract addresses, since we may be
|
|
290
|
-
// performing a raw token transaction and the parameter
|
|
291
|
-
// applies to the token and not the native amount.
|
|
292
|
-
//
|
|
293
|
-
// Contracts have nondeterministic gas most of the time
|
|
294
|
-
// versus estimations, anyway.
|
|
295
|
-
const isSendAllBaseAsset = isSendAll && !isToken && !isContractToAddress
|
|
296
|
-
|
|
297
|
-
// For native send all transactions, we have to make sure that
|
|
298
|
-
// the `tipGasPrice` is equal to the `gasPrice`, since this is
|
|
299
|
-
// effectively like saying that the `maxFeePerGas` is equal
|
|
300
|
-
// to the `maxPriorityFeePerGas`. We do this so that for a
|
|
301
|
-
// fixed gas cost transaction, no dust balance should remain,
|
|
302
|
-
// since any deviation in the underlying `baseFeePerGas` will
|
|
303
|
-
// result only affect the tip for the miner - no dust remains.
|
|
304
|
-
const tipGasPrice =
|
|
305
|
-
providedTipGasPrice ??
|
|
306
|
-
(eip1559Enabled && isSendAllBaseAsset ? resolvedGasPrice : maybeTipGasPrice)
|
|
307
|
-
|
|
308
|
-
const gasPrice = eip1559Enabled
|
|
309
|
-
? ensureSaneEip1559GasPriceForTipGasPrice({
|
|
310
|
-
gasPrice: resolvedGasPrice,
|
|
311
|
-
tipGasPrice,
|
|
312
|
-
})
|
|
313
|
-
: resolvedGasPrice
|
|
314
|
-
|
|
315
|
-
const nonce =
|
|
316
|
-
providedNonce ?? (await asset.baseAsset.getNonce({ asset, fromAddress, walletAccount }))
|
|
322
|
+
eip1559Enabled,
|
|
323
|
+
resolvedGasPrice: gasPrice,
|
|
324
|
+
resolvedTipGasPrice: tipGasPrice,
|
|
325
|
+
} = resolveTxFactoryGasPrices({
|
|
326
|
+
customFee: providedCustomFee,
|
|
327
|
+
feeData,
|
|
328
|
+
gasPrice: providedGasPrice,
|
|
329
|
+
tipGasPrice: providedTipGasPrice,
|
|
330
|
+
})
|
|
317
331
|
|
|
318
332
|
if (nft) {
|
|
319
333
|
const {
|
|
@@ -327,55 +341,81 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
327
341
|
toAddress: providedToAddress,
|
|
328
342
|
})
|
|
329
343
|
|
|
330
|
-
const
|
|
344
|
+
const resolvedTxAttributes = await resolveTxAttributesByTxType({
|
|
345
|
+
asset: asset.baseAsset,
|
|
346
|
+
assetClientInterface,
|
|
347
|
+
fromAddress,
|
|
348
|
+
nonce: providedNonce,
|
|
349
|
+
txInput,
|
|
350
|
+
toAddress: providedToAddress,
|
|
351
|
+
txToAddress,
|
|
352
|
+
txType,
|
|
353
|
+
walletAccount,
|
|
354
|
+
})
|
|
331
355
|
|
|
332
356
|
return createUnsignedTxWithFees({
|
|
357
|
+
...resolvedTxAttributes,
|
|
333
358
|
chainId,
|
|
334
359
|
asset,
|
|
335
|
-
to: txToAddress,
|
|
336
|
-
value,
|
|
337
|
-
data: txInput,
|
|
338
360
|
gasLimit,
|
|
339
361
|
gasPrice,
|
|
340
362
|
tipGasPrice,
|
|
341
|
-
nonce,
|
|
342
|
-
coinAmount: value,
|
|
343
|
-
fromAddress,
|
|
344
|
-
toAddress,
|
|
345
363
|
eip1559Enabled,
|
|
346
364
|
})
|
|
347
365
|
}
|
|
348
366
|
|
|
349
|
-
const
|
|
367
|
+
const resolvedTxAttributes = await resolveTxAttributesByTxType({
|
|
368
|
+
asset,
|
|
369
|
+
assetClientInterface,
|
|
370
|
+
amount: providedAmount,
|
|
371
|
+
fromAddress,
|
|
372
|
+
nonce: providedNonce,
|
|
373
|
+
txInput: providedTxInput,
|
|
374
|
+
toAddress: providedToAddress,
|
|
375
|
+
txType,
|
|
376
|
+
walletAccount,
|
|
377
|
+
})
|
|
350
378
|
|
|
351
|
-
|
|
352
|
-
|
|
379
|
+
// TODO: `gasLimit` should become `txGasLimit` and returned
|
|
380
|
+
// by `resolveTxAttributesByTxType`.
|
|
353
381
|
const gasLimit =
|
|
354
382
|
providedGasLimit ??
|
|
355
383
|
(await fetchGasLimit({
|
|
356
384
|
asset,
|
|
357
385
|
feeData,
|
|
358
386
|
fromAddress,
|
|
359
|
-
toAddress:
|
|
360
|
-
txInput:
|
|
361
|
-
contractAddress: txToAddress,
|
|
387
|
+
toAddress: resolvedTxAttributes.toAddress,
|
|
388
|
+
txInput: resolvedTxAttributes.txInput,
|
|
389
|
+
contractAddress: resolvedTxAttributes.txToAddress,
|
|
362
390
|
bip70,
|
|
363
|
-
amount,
|
|
391
|
+
amount: resolvedTxAttributes.amount,
|
|
392
|
+
txType,
|
|
364
393
|
}))
|
|
365
394
|
|
|
395
|
+
// HACK: We cannot ensure the no dust invariant for `isSendAll`
|
|
396
|
+
// transactions to contract addresses, since we may be
|
|
397
|
+
// performing a raw token transaction and the parameter
|
|
398
|
+
// applies to the token and not the native amount.
|
|
399
|
+
//
|
|
400
|
+
// Contracts have nondeterministic gas most of the time
|
|
401
|
+
// versus estimations, anyway.
|
|
402
|
+
const isSendAllBaseAsset =
|
|
403
|
+
isSendAll && !isEthereumLikeToken(asset) && !resolvedTxAttributes.isContractTxToAddress
|
|
404
|
+
|
|
366
405
|
return createUnsignedTxWithFees({
|
|
406
|
+
...resolvedTxAttributes,
|
|
367
407
|
asset,
|
|
368
408
|
chainId,
|
|
369
|
-
to: txToAddress,
|
|
370
|
-
value,
|
|
371
|
-
data: txInput,
|
|
372
409
|
gasLimit,
|
|
373
410
|
gasPrice,
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
411
|
+
// For native send all transactions, we have to make sure that
|
|
412
|
+
// the `tipGasPrice` is equal to the `gasPrice`, since this is
|
|
413
|
+
// effectively like saying that the `maxFeePerGas` is equal
|
|
414
|
+
// to the `maxPriorityFeePerGas`. We do this so that for a
|
|
415
|
+
// fixed gas cost transaction, no dust balance should remain,
|
|
416
|
+
// since any deviation in the underlying `baseFeePerGas` will
|
|
417
|
+
// result only affect the tip for the miner - no dust remains.
|
|
418
|
+
tipGasPrice: isSendAllBaseAsset && eip1559Enabled ? gasPrice : tipGasPrice,
|
|
379
419
|
eip1559Enabled,
|
|
380
420
|
})
|
|
381
421
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SynchronizedTime } from '@exodus/basic-utils'
|
|
1
2
|
import NumberUnit from '@exodus/currency'
|
|
2
3
|
import { isEthereumLikeToken, parseUnsignedTx } from '@exodus/ethereum-lib'
|
|
3
4
|
import { bufferToHex } from '@exodus/ethereumjs/util'
|
|
@@ -33,6 +34,7 @@ export const getOptimisticTxLogEffects = async ({
|
|
|
33
34
|
asset,
|
|
34
35
|
assetClientInterface,
|
|
35
36
|
confirmations = 0,
|
|
37
|
+
date = SynchronizedTime.now(),
|
|
36
38
|
fromAddress,
|
|
37
39
|
txId,
|
|
38
40
|
unsignedTx,
|
|
@@ -49,7 +51,7 @@ export const getOptimisticTxLogEffects = async ({
|
|
|
49
51
|
// this converts an transactionBuffer to values we can use when creating the tx logs
|
|
50
52
|
const parsedTx = parseUnsignedTx({ asset, unsignedTx })
|
|
51
53
|
|
|
52
|
-
const { nonce } = parsedTx
|
|
54
|
+
const { nonce, to } = parsedTx
|
|
53
55
|
assert(Number.isInteger(nonce), 'expected integer nonce')
|
|
54
56
|
|
|
55
57
|
const amount = parsedTx.amount || asset.currency.ZERO
|
|
@@ -66,13 +68,12 @@ export const getOptimisticTxLogEffects = async ({
|
|
|
66
68
|
const gasLimit = parsedTx.gasLimit
|
|
67
69
|
assert(Number.isInteger(gasLimit), 'expected integer gasLimit')
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
assert(typeof toAddress === 'string', 'expected string toAddress')
|
|
71
|
+
if (to) assert(typeof to === 'string', 'expected string toAddress')
|
|
71
72
|
|
|
72
73
|
const data = parsedTx.data
|
|
73
74
|
const methodId = data ? bufferToHex(data).slice(0, 10) : undefined
|
|
74
75
|
|
|
75
|
-
const selfSend = fromAddress.toLowerCase() ===
|
|
76
|
+
const selfSend = fromAddress.toLowerCase() === to?.toLowerCase()
|
|
76
77
|
|
|
77
78
|
const baseAsset = asset.baseAsset
|
|
78
79
|
|
|
@@ -92,10 +93,11 @@ export const getOptimisticTxLogEffects = async ({
|
|
|
92
93
|
|
|
93
94
|
const sharedProps = {
|
|
94
95
|
confirmations,
|
|
96
|
+
date,
|
|
95
97
|
feeAmount,
|
|
96
98
|
feeCoinName: asset.feeAsset.name,
|
|
97
99
|
selfSend,
|
|
98
|
-
to
|
|
100
|
+
to,
|
|
99
101
|
txId,
|
|
100
102
|
data: {
|
|
101
103
|
gasLimit,
|
package/src/tx-send/tx-send.js
CHANGED
|
@@ -3,8 +3,8 @@ import assert from 'minimalistic-assert'
|
|
|
3
3
|
|
|
4
4
|
import * as ErrorWrapper from '../error-wrapper.js'
|
|
5
5
|
import { transactionExists } from '../eth-like-util.js'
|
|
6
|
-
import { ARBITRARY_ADDRESS } from '../gas-estimation.js'
|
|
7
6
|
import { getOptimisticTxLogEffects } from '../tx-log/index.js'
|
|
7
|
+
import { ARBITRARY_ADDRESS } from '../tx-type/index.js'
|
|
8
8
|
|
|
9
9
|
const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
10
10
|
assert(assetClientInterface, 'assetClientInterface is required')
|
|
@@ -40,13 +40,13 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
40
40
|
const { unsignedTx } = await resolveUnsignedTx()
|
|
41
41
|
|
|
42
42
|
const parsedTx = parseUnsignedTx({ asset, unsignedTx })
|
|
43
|
-
const toAddress = parsedTx.to
|
|
44
43
|
|
|
45
44
|
// unknown data from buffer...
|
|
45
|
+
const to = parsedTx.to
|
|
46
46
|
const fromAddress = unsignedTx.txMeta.fromAddress
|
|
47
47
|
|
|
48
48
|
assert(
|
|
49
|
-
|
|
49
|
+
to?.toLowerCase() !== ARBITRARY_ADDRESS,
|
|
50
50
|
`The receiving wallet address must not be ${ARBITRARY_ADDRESS}`
|
|
51
51
|
)
|
|
52
52
|
|
|
@@ -140,7 +140,6 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
140
140
|
walletAccount,
|
|
141
141
|
})
|
|
142
142
|
|
|
143
|
-
// NOTE: `optimisticTxLogEffects` **must** be written sequentially.
|
|
144
143
|
for (const optimisticTxLogEffect of optimisticTxLogEffects) {
|
|
145
144
|
await assetClientInterface.updateTxLogAndNotify(optimisticTxLogEffect)
|
|
146
145
|
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import NumberUnit from '@exodus/currency'
|
|
2
|
+
import { isEthereumLikeToken } from '@exodus/ethereum-lib'
|
|
3
|
+
import { bufferToHex } from '@exodus/ethereumjs/util'
|
|
4
|
+
import assert from 'minimalistic-assert'
|
|
5
|
+
|
|
6
|
+
import { isContractAddressCached } from '../eth-like-util.js'
|
|
7
|
+
|
|
8
|
+
export const TX_TYPE_TRANSFER = 'transfer'
|
|
9
|
+
export const TX_TYPE_CREATE_CONTRACT = 'create-contract'
|
|
10
|
+
|
|
11
|
+
const VALID_TX_TYPES = new Set([TX_TYPE_TRANSFER, TX_TYPE_CREATE_CONTRACT])
|
|
12
|
+
|
|
13
|
+
export const isValidTxType = (txType) => VALID_TX_TYPES.has(txType)
|
|
14
|
+
|
|
15
|
+
// TODO: Remove this.
|
|
16
|
+
// HACK: If a recipient address is not defined, we usually fall back to
|
|
17
|
+
// default address so gas estimation can still complete successfully
|
|
18
|
+
// without knowledge of which accounts are involved.
|
|
19
|
+
//
|
|
20
|
+
// However, we must be careful to select addresses which are unlikely
|
|
21
|
+
// to have existing obligations, such as popular dead addresses or the
|
|
22
|
+
// reserved addresses of precompiles, since these can influence gas
|
|
23
|
+
// estimation.
|
|
24
|
+
//
|
|
25
|
+
// Here, we use an address which is mostly all `1`s to make sure we can
|
|
26
|
+
// exaggerate the worst-case calldata cost (which is priced per high bit)
|
|
27
|
+
// whilst being unlikely to have any token balances.
|
|
28
|
+
//
|
|
29
|
+
// Unfortunately, we can't use `0xffffffffffffffffffffffffffffffffffffffff`,
|
|
30
|
+
// since this address is a whale.
|
|
31
|
+
export const ARBITRARY_ADDRESS = '0xfffFfFfFfFfFFFFFfeFfFFFffFffFFFFfFFFFFFF'.toLowerCase()
|
|
32
|
+
|
|
33
|
+
export const assertCriticalTxAttributes = (criticalTxAttributes) => {
|
|
34
|
+
const { amount, toAddress, txInput, txToAddress, txType, txValue } = criticalTxAttributes
|
|
35
|
+
|
|
36
|
+
assert(amount instanceof NumberUnit, 'expected NumberUnit amount')
|
|
37
|
+
assert(
|
|
38
|
+
typeof txInput === 'string' && txInput.startsWith('0x', 'expected hexadecimal string txInput')
|
|
39
|
+
)
|
|
40
|
+
assert(isValidTxType(txType), 'expected valid txType')
|
|
41
|
+
assert(txValue instanceof NumberUnit, 'expected NumberUnit txValue')
|
|
42
|
+
|
|
43
|
+
if (txType === TX_TYPE_CREATE_CONTRACT) {
|
|
44
|
+
assert(toAddress === null, 'expected null toAddress')
|
|
45
|
+
assert(txToAddress === null, 'expected null txToAddress')
|
|
46
|
+
assert(txInput !== '0x', 'expected non-empty txInput for contract creation')
|
|
47
|
+
} else {
|
|
48
|
+
assert(typeof toAddress === 'string', 'expected string toAddress')
|
|
49
|
+
assert(typeof txToAddress === 'string', 'expected string txToAddress')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return criticalTxAttributes
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const assertTxAttributes = (txAttributes) => {
|
|
56
|
+
// TODO: Note that the distinction between "critical" and "non-critical"
|
|
57
|
+
// only exists to satisfy `fetchGasLimit` needing to know some props.
|
|
58
|
+
// Once we encapsulate `gasLimit`, all properties will be critical.
|
|
59
|
+
const {
|
|
60
|
+
/* critical */
|
|
61
|
+
amount,
|
|
62
|
+
toAddress,
|
|
63
|
+
txInput,
|
|
64
|
+
txToAddress,
|
|
65
|
+
txType,
|
|
66
|
+
txValue,
|
|
67
|
+
/* non-critical */
|
|
68
|
+
fromAddress,
|
|
69
|
+
nonce,
|
|
70
|
+
isContractTxToAddress,
|
|
71
|
+
} = txAttributes
|
|
72
|
+
|
|
73
|
+
assertCriticalTxAttributes({ amount, toAddress, txInput, txToAddress, txType, txValue })
|
|
74
|
+
|
|
75
|
+
assert(typeof fromAddress === 'string', 'expected fromAddress')
|
|
76
|
+
assert(Number.isInteger(nonce), 'expected integer nonce')
|
|
77
|
+
assert(typeof isContractTxToAddress === 'boolean', 'expected boolean isContractTxToAddress')
|
|
78
|
+
|
|
79
|
+
return txAttributes
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const createResolvedTxAttributes = async ({
|
|
83
|
+
amount,
|
|
84
|
+
asset,
|
|
85
|
+
fromAddress,
|
|
86
|
+
nonce,
|
|
87
|
+
toAddress,
|
|
88
|
+
txInput,
|
|
89
|
+
txToAddress,
|
|
90
|
+
txValue,
|
|
91
|
+
txType,
|
|
92
|
+
}) =>
|
|
93
|
+
assertTxAttributes({
|
|
94
|
+
amount,
|
|
95
|
+
fromAddress,
|
|
96
|
+
nonce,
|
|
97
|
+
toAddress,
|
|
98
|
+
txInput,
|
|
99
|
+
txToAddress,
|
|
100
|
+
txValue,
|
|
101
|
+
txType,
|
|
102
|
+
isContractTxToAddress:
|
|
103
|
+
Boolean(txToAddress) && (await isContractAddressCached({ asset, address: txToAddress })),
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
export const resolveTxFromAddress = async ({
|
|
107
|
+
asset,
|
|
108
|
+
assetClientInterface,
|
|
109
|
+
fromAddress: providedFromAddress,
|
|
110
|
+
walletAccount,
|
|
111
|
+
}) => {
|
|
112
|
+
assert(asset, 'expected asset')
|
|
113
|
+
assert(assetClientInterface, 'expected assetClientInterface')
|
|
114
|
+
assert(walletAccount, 'expected walletAccount')
|
|
115
|
+
|
|
116
|
+
if (providedFromAddress) return providedFromAddress
|
|
117
|
+
|
|
118
|
+
return assetClientInterface.getReceiveAddress({
|
|
119
|
+
assetName: asset.baseAsset.name,
|
|
120
|
+
walletAccount,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const resolveTxNonce = async ({ asset, fromAddress, nonce: providedNonce, walletAccount }) => {
|
|
125
|
+
assert(asset, 'expected asset')
|
|
126
|
+
assert(typeof fromAddress === 'string', 'expected string fromAddress')
|
|
127
|
+
assert(walletAccount, 'expected walletAccount')
|
|
128
|
+
|
|
129
|
+
if (Number.isInteger(providedNonce)) return providedNonce
|
|
130
|
+
|
|
131
|
+
return asset.baseAsset.getNonce({ asset, fromAddress, walletAccount })
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const resolveTxInput = ({ asset, toAddress, amount, txInput: providedTxInput }) => {
|
|
135
|
+
assert(asset, 'expected asset')
|
|
136
|
+
assert(typeof toAddress === 'string', 'expected string toAddress')
|
|
137
|
+
assert(amount instanceof NumberUnit, 'expected NumberUnit amount')
|
|
138
|
+
|
|
139
|
+
if (providedTxInput) return bufferToHex(providedTxInput)
|
|
140
|
+
|
|
141
|
+
if (!isEthereumLikeToken(asset)) return '0x'
|
|
142
|
+
|
|
143
|
+
return bufferToHex(asset.contract.transfer.build(toAddress.toLowerCase(), amount?.toBaseString()))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const resolveCriticalTxAttributes = ({
|
|
147
|
+
asset,
|
|
148
|
+
amount: providedAmount,
|
|
149
|
+
toAddress: providedToAddress,
|
|
150
|
+
txToAddress: providedTxToAddress,
|
|
151
|
+
txInput: providedTxInput,
|
|
152
|
+
txType,
|
|
153
|
+
}) => {
|
|
154
|
+
assert(asset, 'expected asset')
|
|
155
|
+
assert(isValidTxType(txType), 'expected valid txType')
|
|
156
|
+
|
|
157
|
+
const amount = providedAmount ?? asset.currency.ZERO
|
|
158
|
+
assert(amount instanceof NumberUnit, 'expected providedAmount')
|
|
159
|
+
|
|
160
|
+
if (txType === TX_TYPE_CREATE_CONTRACT) {
|
|
161
|
+
assert(asset.name === asset.baseAsset.name, 'must use baseAsset for contract deployments')
|
|
162
|
+
assert(!providedToAddress, 'toAddress must be falsy when creating a contract')
|
|
163
|
+
assert(!providedTxToAddress, 'txToAddress must be falsy when creating a contract')
|
|
164
|
+
|
|
165
|
+
return assertCriticalTxAttributes({
|
|
166
|
+
amount,
|
|
167
|
+
toAddress: null,
|
|
168
|
+
txInput: providedTxInput,
|
|
169
|
+
txToAddress: null,
|
|
170
|
+
txType,
|
|
171
|
+
txValue: amount,
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
assert(txType === TX_TYPE_TRANSFER, 'expected TX_TYPE_TRANSFER')
|
|
176
|
+
|
|
177
|
+
// HACK: If a `toAddress` hasn't been defined, then we
|
|
178
|
+
// fall back to the `ARBITRARY_ADDRESS`. Note that
|
|
179
|
+
// this should only be used to help determine the
|
|
180
|
+
// fee of a transaction where we don't yet know the
|
|
181
|
+
// intended recipient. Attempts to send to this
|
|
182
|
+
// sentinel address will ultimately `throw`.
|
|
183
|
+
const toAddress = providedToAddress || ARBITRARY_ADDRESS
|
|
184
|
+
const txValue = isEthereumLikeToken(asset) ? asset.baseAsset.currency.ZERO : amount
|
|
185
|
+
const txInput = resolveTxInput({ amount, asset, txInput: providedTxInput, toAddress })
|
|
186
|
+
|
|
187
|
+
const baseProps = {
|
|
188
|
+
amount,
|
|
189
|
+
toAddress,
|
|
190
|
+
txInput,
|
|
191
|
+
txType,
|
|
192
|
+
txValue,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// When CEX swapping a main asset, the exchange may provide a txInput and an address. Use this address as the tx 'to' address.
|
|
196
|
+
// When DEX swapping a main asset, the exchange provides a txInput and a DEX address. Use the DEX address as the tx 'to' address.
|
|
197
|
+
// When sending a main asset, the transaction (tx) 'to' address is the receiver's address. No tx input is provided.
|
|
198
|
+
if (!isEthereumLikeToken(asset)) {
|
|
199
|
+
return assertCriticalTxAttributes({
|
|
200
|
+
...baseProps,
|
|
201
|
+
txToAddress: providedTxToAddress || toAddress,
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (providedTxInput) {
|
|
206
|
+
// When CEX swapping a token, the exchange may provide a txInput but not an address. In this case, the tx 'to' address is the token address.
|
|
207
|
+
// When DEX swapping a token, the exchange provides a txInput and a DEX address. Use the DEX address as the tx 'to' address.
|
|
208
|
+
const txToAddress = providedTxToAddress || providedToAddress || asset.contract?.address
|
|
209
|
+
return assertCriticalTxAttributes({ ...baseProps, txToAddress })
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// When sending a token, the tx 'to' address is the asset.contract?.address. No tx input is provided; it's resolved locally.
|
|
213
|
+
const txToAddress = asset.contract?.address
|
|
214
|
+
return assertCriticalTxAttributes({ ...baseProps, txToAddress })
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Normalizes the properties of transactions that can
|
|
218
|
+
// vary depending upon the `txType` and `asset` to
|
|
219
|
+
// ensure these evaluate consistently.
|
|
220
|
+
export const resolveTxAttributesByTxType = async ({
|
|
221
|
+
asset,
|
|
222
|
+
assetClientInterface,
|
|
223
|
+
amount: providedAmount,
|
|
224
|
+
fromAddress: providedFromAddress,
|
|
225
|
+
nonce: providedNonce,
|
|
226
|
+
txInput: providedTxInput,
|
|
227
|
+
txToAddress: providedTxToAddress,
|
|
228
|
+
toAddress: providedToAddress,
|
|
229
|
+
txType,
|
|
230
|
+
walletAccount,
|
|
231
|
+
}) => {
|
|
232
|
+
assert(isValidTxType(txType), 'expected valid txType')
|
|
233
|
+
|
|
234
|
+
const fromAddress = await resolveTxFromAddress({
|
|
235
|
+
asset,
|
|
236
|
+
assetClientInterface,
|
|
237
|
+
fromAddress: providedFromAddress,
|
|
238
|
+
walletAccount,
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const nonce = await resolveTxNonce({
|
|
242
|
+
asset,
|
|
243
|
+
fromAddress,
|
|
244
|
+
nonce: providedNonce,
|
|
245
|
+
walletAccount,
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
return createResolvedTxAttributes({
|
|
249
|
+
asset,
|
|
250
|
+
fromAddress,
|
|
251
|
+
nonce,
|
|
252
|
+
...resolveCriticalTxAttributes({
|
|
253
|
+
asset,
|
|
254
|
+
amount: providedAmount,
|
|
255
|
+
toAddress: providedToAddress,
|
|
256
|
+
txInput: providedTxInput,
|
|
257
|
+
txToAddress: providedTxToAddress,
|
|
258
|
+
txType,
|
|
259
|
+
}),
|
|
260
|
+
})
|
|
261
|
+
}
|