@exodus/ethereum-api 8.76.3 → 8.76.5
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 +2 -2
- package/src/check-tx/create-assess-transaction.js +23 -0
- package/src/check-tx/create-check-tx.js +12 -24
- package/src/create-asset.js +14 -10
- package/src/gas-estimation.js +10 -1
- package/src/move-funds.js +56 -23
- package/src/send-validations.js +1 -2
- package/src/tx-create.js +143 -29
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.76.5](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.4...@exodus/ethereum-api@8.76.5) (2026-06-03)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @exodus/ethereum-api
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [8.76.4](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.3...@exodus/ethereum-api@8.76.4) (2026-06-02)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
* fix(ethereum-api): eliminate send-all dust on HYPE and other forceGasLimitEstimation networks (#7747)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
## [8.76.3](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.76.2...@exodus/ethereum-api@8.76.3) (2026-06-01)
|
|
7
25
|
|
|
8
26
|
**Note:** Version bump only for package @exodus/ethereum-api
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.76.
|
|
3
|
+
"version": "8.76.5",
|
|
4
4
|
"description": "Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Ethereum and EVM-based blockchains",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"type": "git",
|
|
71
71
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "651f8346570d40e724109e3df1cd2a3e10323a6f"
|
|
74
74
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { fetchival } from '@exodus/fetch'
|
|
2
|
+
import assert from 'minimalistic-assert'
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_ASSESS_TRANSACTION_API_URL =
|
|
5
|
+
'https://simulation.a.exodus.io/simulation/transaction/assessment'
|
|
6
|
+
|
|
7
|
+
export const createAssessTransaction = (
|
|
8
|
+
{ apiUrl = DEFAULT_ASSESS_TRANSACTION_API_URL, headers, request = fetchival } = Object.create(
|
|
9
|
+
null
|
|
10
|
+
)
|
|
11
|
+
) => {
|
|
12
|
+
assert(typeof apiUrl === 'string' && apiUrl.length > 0, 'apiUrl must be a non-empty string')
|
|
13
|
+
|
|
14
|
+
return async function assessTransaction({ payload }) {
|
|
15
|
+
return request(apiUrl, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: {
|
|
18
|
+
...headers,
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
},
|
|
21
|
+
}).post(payload)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { memoizeLruCache } from '@exodus/asset-lib'
|
|
2
|
-
import { makeSimulationAPICall } from '@exodus/web3-utils'
|
|
3
2
|
import assert from 'minimalistic-assert'
|
|
4
3
|
import ms from 'ms'
|
|
5
4
|
|
|
@@ -37,25 +36,25 @@ const isRecipientFinding = ({ finding, recipientAddress }) => {
|
|
|
37
36
|
})
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
const
|
|
41
|
-
if (!Array.isArray(findings) || !recipientAddress) return
|
|
42
|
-
|
|
43
|
-
isRecipientFinding({ finding: candidate, recipientAddress })
|
|
44
|
-
)
|
|
45
|
-
return finding?.title || GENERIC_RISK_REASON
|
|
39
|
+
const findRecipientFinding = ({ findings, recipientAddress }) => {
|
|
40
|
+
if (!Array.isArray(findings) || !recipientAddress) return
|
|
41
|
+
return findings.find((candidate) => isRecipientFinding({ finding: candidate, recipientAddress }))
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
const parseAssessment = ({ body, recipientAddress }) => {
|
|
49
45
|
if (!body?.success) return { action: 'NONE' }
|
|
50
46
|
if (body.data?.recommendation !== 'deny') return { action: 'NONE' }
|
|
47
|
+
const finding = findRecipientFinding({ findings: body.data?.findings, recipientAddress })
|
|
48
|
+
if (!finding) return { action: 'NONE' }
|
|
49
|
+
|
|
51
50
|
return {
|
|
52
51
|
action: 'WARN',
|
|
53
|
-
reason:
|
|
52
|
+
reason: finding.title || GENERIC_RISK_REASON,
|
|
54
53
|
}
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
// Rejects after `timeoutMs`. The underlying request keeps running because
|
|
58
|
-
//
|
|
56
|
+
// Rejects after `timeoutMs`. The underlying request keeps running because the
|
|
57
|
+
// API helper doesn't accept an AbortSignal.
|
|
59
58
|
const withTimeout = async (promise, timeoutMs) => {
|
|
60
59
|
let timeoutId
|
|
61
60
|
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
@@ -71,14 +70,9 @@ const withTimeout = async (promise, timeoutMs) => {
|
|
|
71
70
|
const noopLogger = { warn: () => {} }
|
|
72
71
|
|
|
73
72
|
export const createCheckTx = (
|
|
74
|
-
{
|
|
75
|
-
apiUrl,
|
|
76
|
-
timeout = DEFAULT_TIMEOUT_MS,
|
|
77
|
-
makeApiCall = makeSimulationAPICall,
|
|
78
|
-
logger = noopLogger,
|
|
79
|
-
} = Object.create(null)
|
|
73
|
+
{ timeout = DEFAULT_TIMEOUT_MS, makeApiCall, logger = noopLogger } = Object.create(null)
|
|
80
74
|
) => {
|
|
81
|
-
assert(
|
|
75
|
+
assert(typeof makeApiCall === 'function', 'makeApiCall is required')
|
|
82
76
|
|
|
83
77
|
const warn = typeof logger?.warn === 'function' ? logger.warn.bind(logger) : noopLogger.warn
|
|
84
78
|
|
|
@@ -90,13 +84,7 @@ export const createCheckTx = (
|
|
|
90
84
|
if (input !== undefined) transaction.input = input
|
|
91
85
|
if (hash !== undefined) transaction.hash = hash
|
|
92
86
|
|
|
93
|
-
const body = await withTimeout(
|
|
94
|
-
makeApiCall({
|
|
95
|
-
url: apiUrl,
|
|
96
|
-
payload: { serviceProvider: 'hypernative', transaction },
|
|
97
|
-
}),
|
|
98
|
-
timeout
|
|
99
|
-
)
|
|
87
|
+
const body = await withTimeout(makeApiCall({ payload: { transaction } }), timeout)
|
|
100
88
|
|
|
101
89
|
if (!body) throw new Error('checkTx: empty response')
|
|
102
90
|
return parseAssessment({ body, recipientAddress: toAddress })
|
package/src/create-asset.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createConsoleLogger } from '@exodus/asset-lib'
|
|
1
2
|
import { ASSET_FAMILY, connectAssetsList } from '@exodus/assets'
|
|
2
3
|
import bip44Constants from '@exodus/bip44-constants/by-ticker.js'
|
|
3
4
|
import {
|
|
@@ -21,6 +22,7 @@ import lodash from 'lodash'
|
|
|
21
22
|
import assert from 'minimalistic-assert'
|
|
22
23
|
|
|
23
24
|
import { addressHasHistoryFactory } from './address-has-history.js'
|
|
25
|
+
import { createAssessTransaction } from './check-tx/create-assess-transaction.js'
|
|
24
26
|
import { createCheckTx } from './check-tx/index.js'
|
|
25
27
|
import {
|
|
26
28
|
createGetBlackListStatus,
|
|
@@ -78,9 +80,9 @@ export const createAssetFactory = ({
|
|
|
78
80
|
useAbsoluteBalanceAndNonce = false,
|
|
79
81
|
delisted = false,
|
|
80
82
|
privacyRpcUrl: defaultPrivacyRpcUrl,
|
|
81
|
-
riskAssessment: defaultRiskAssessment,
|
|
82
83
|
wsGatewayUri: defaultWsGatewayUri,
|
|
83
84
|
eip7702Supported,
|
|
85
|
+
transactionAssessment: defaultTransactionAssessment,
|
|
84
86
|
}) => {
|
|
85
87
|
assert(assetsList, 'assetsList is required')
|
|
86
88
|
assert(providedFeeData || feeDataConfig, 'feeData or feeDataConfig is required')
|
|
@@ -104,7 +106,7 @@ export const createAssetFactory = ({
|
|
|
104
106
|
nfts: defaultNfts,
|
|
105
107
|
customTokens: defaultCustomTokens,
|
|
106
108
|
privacyRpcUrl: defaultPrivacyRpcUrl,
|
|
107
|
-
|
|
109
|
+
transactionAssessment: defaultTransactionAssessment,
|
|
108
110
|
}
|
|
109
111
|
return (
|
|
110
112
|
{
|
|
@@ -125,7 +127,7 @@ export const createAssetFactory = ({
|
|
|
125
127
|
supportsCustomFees,
|
|
126
128
|
useAbsoluteBalanceAndNonce: overrideUseAbsoluteBalanceAndNonce,
|
|
127
129
|
privacyRpcUrl,
|
|
128
|
-
|
|
130
|
+
transactionAssessment,
|
|
129
131
|
} = configWithOverrides
|
|
130
132
|
|
|
131
133
|
const asset = assets[base.name]
|
|
@@ -276,7 +278,7 @@ export const createAssetFactory = ({
|
|
|
276
278
|
})
|
|
277
279
|
|
|
278
280
|
const getFeeAsync = async (...args) => {
|
|
279
|
-
const { unsignedTx, ...rest } = await createTx(...args)
|
|
281
|
+
const { unsignedTx, amount, ...rest } = await createTx(...args)
|
|
280
282
|
return rest
|
|
281
283
|
}
|
|
282
284
|
|
|
@@ -294,16 +296,18 @@ export const createAssetFactory = ({
|
|
|
294
296
|
|
|
295
297
|
const securityChecks = createSecurityChecks({ eip7702Supported })
|
|
296
298
|
|
|
299
|
+
const web3 = createWeb3API({ asset })
|
|
300
|
+
|
|
297
301
|
let checkTx
|
|
298
|
-
|
|
302
|
+
let sendValidations = []
|
|
303
|
+
if (transactionAssessment?.enabled) {
|
|
299
304
|
checkTx = createCheckTx({
|
|
300
|
-
apiUrl:
|
|
301
|
-
logger:
|
|
305
|
+
makeApiCall: createAssessTransaction({ apiUrl: transactionAssessment.apiUrl }),
|
|
306
|
+
logger: createConsoleLogger('@exodus/ethereum-api:check-tx'),
|
|
302
307
|
})
|
|
308
|
+
sendValidations = createSendValidations({ assetClientInterface, checkTx })
|
|
303
309
|
}
|
|
304
310
|
|
|
305
|
-
const sendValidations = checkTx ? createSendValidations({ assetClientInterface, checkTx }) : []
|
|
306
|
-
|
|
307
311
|
const moveFunds = moveFundsFactory({
|
|
308
312
|
baseAssetName: asset.name,
|
|
309
313
|
assetClientInterface,
|
|
@@ -365,7 +369,7 @@ export const createAssetFactory = ({
|
|
|
365
369
|
}),
|
|
366
370
|
}),
|
|
367
371
|
validateAssetId: address.validate,
|
|
368
|
-
web3
|
|
372
|
+
web3,
|
|
369
373
|
}
|
|
370
374
|
|
|
371
375
|
const fullAsset = {
|
package/src/gas-estimation.js
CHANGED
|
@@ -119,6 +119,7 @@ export async function fetchGasLimit({
|
|
|
119
119
|
bip70,
|
|
120
120
|
txType = TX_TYPE_TRANSFER,
|
|
121
121
|
throwOnError = true,
|
|
122
|
+
isSendAll = false,
|
|
122
123
|
}) {
|
|
123
124
|
if (bip70?.bitpay?.data && bip70?.bitpay?.gasPrice) {
|
|
124
125
|
// from on chain stats https://dune.xyz/queries/189123
|
|
@@ -178,7 +179,15 @@ export async function fetchGasLimit({
|
|
|
178
179
|
})
|
|
179
180
|
|
|
180
181
|
const scaledGasLimitEstimate = scaleGasLimitEstimate({ estimatedGasLimit, gasLimitMultiplier })
|
|
181
|
-
if (!isToken)
|
|
182
|
+
if (!isToken) {
|
|
183
|
+
// For native send-all on EOA targets, return the raw `eth_estimateGas`
|
|
184
|
+
// result (no 2x safety pad). Combined with the EIP-1559 tip override
|
|
185
|
+
// applied in tx-create.js (also gated on `isSendAll && !isContract`),
|
|
186
|
+
// this is the no-dust path: gasLimit ≈ gasUsed, so nothing comes back
|
|
187
|
+
// as refund. For contracts we keep the safety pad because contract
|
|
188
|
+
// gas can be nondeterministic against estimates.
|
|
189
|
+
return isSendAll && !isContractTxToAddress ? estimatedGasLimit : scaledGasLimitEstimate
|
|
190
|
+
}
|
|
182
191
|
|
|
183
192
|
// NOTE: If we've enabled `fixGasLimit`s for a token,
|
|
184
193
|
// we need to make sure that transaction we're
|
package/src/move-funds.js
CHANGED
|
@@ -4,6 +4,7 @@ import assert from 'minimalistic-assert'
|
|
|
4
4
|
|
|
5
5
|
import { getNonce, getTokenBalanceFromNode } from './eth-like-util.js'
|
|
6
6
|
import { fetchGasLimit } from './gas-estimation.js'
|
|
7
|
+
import { AmountIncludesFeeUnderflowError } from './tx-create.js'
|
|
7
8
|
|
|
8
9
|
export const moveFundsFactory = ({ baseAssetName, assetClientInterface, createTx, server }) => {
|
|
9
10
|
assert(baseAssetName, 'baseAssetName is required')
|
|
@@ -73,37 +74,69 @@ export const moveFundsFactory = ({ baseAssetName, assetClientInterface, createTx
|
|
|
73
74
|
fromAddress,
|
|
74
75
|
toAddress,
|
|
75
76
|
amount,
|
|
77
|
+
// `isSendAll` and `amountIncludesFee` are no-ops on the token branch
|
|
78
|
+
// (both gated on `!isToken` / `!isEthereumLikeToken(asset)` downstream),
|
|
79
|
+
// so don't pass them. Keeps the API surface honest about which flags
|
|
80
|
+
// actually take effect for each branch.
|
|
81
|
+
...(isToken ? null : { isSendAll: true }),
|
|
76
82
|
})
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
let createTxResult
|
|
85
|
+
try {
|
|
86
|
+
createTxResult = await createTx({
|
|
87
|
+
asset,
|
|
88
|
+
walletAccount,
|
|
89
|
+
fromAddress,
|
|
90
|
+
// Native sweep: pass the full balance and let createTx subtract the
|
|
91
|
+
// accurate fee. Token sweep: pass the full token balance unchanged
|
|
92
|
+
// (the native gas fee is paid separately from native balance).
|
|
93
|
+
address: toAddress,
|
|
94
|
+
amount,
|
|
95
|
+
nonce,
|
|
96
|
+
gasLimit,
|
|
97
|
+
gasPrice: feeData.gasPrice,
|
|
98
|
+
// TODO: drop this. Move funds has no fee picker, so it's always
|
|
99
|
+
// `feeData.gasPrice` (same as `gasPrice` above) and a no-op anyway.
|
|
100
|
+
// If we ever need a custom fee, thread it through prepareSendFundsTx.
|
|
101
|
+
customFee: feeData.gasPrice,
|
|
102
|
+
// Both flags are no-ops for tokens (see fetchGasLimit call above).
|
|
103
|
+
...(isToken ? null : { isSendAll: true, amountIncludesFee: true }),
|
|
104
|
+
})
|
|
105
|
+
} catch (err) {
|
|
106
|
+
// createTx throws `AmountIncludesFeeUnderflowError` for the native
|
|
107
|
+
// sweep when the accurate fee (including OP-stack L1 data fee) exceeds
|
|
108
|
+
// the balance we passed in. Translate to the typed MoveFundsError so
|
|
109
|
+
// consumers (mobile/desktop MoveFunds screens) render the right copy.
|
|
110
|
+
if (!isToken && err instanceof AmountIncludesFeeUnderflowError) {
|
|
87
111
|
throw new MoveFundsError('balance-negative', { fromAddress })
|
|
88
112
|
}
|
|
89
113
|
|
|
90
|
-
|
|
114
|
+
throw err
|
|
91
115
|
}
|
|
92
116
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
gasPrice: feeData.gasPrice,
|
|
102
|
-
customFee: feeData.gasPrice,
|
|
103
|
-
isSendAll: true,
|
|
104
|
-
})
|
|
117
|
+
// Token sweep equivalent of the native underflow gate: createTx doesn't
|
|
118
|
+
// validate ETH balance vs gas for token transfers, so check it here using
|
|
119
|
+
// the authoritative fee returned from `calculateFee`. This catches the
|
|
120
|
+
// OP-stack edge case where balance covers `gasPrice * gasLimit` but not
|
|
121
|
+
// the additional L1 data fee.
|
|
122
|
+
if (isToken && ethBalance.sub(createTxResult.fee).isNegative) {
|
|
123
|
+
throw new MoveFundsError('token-fee-insufficient', { fromAddress })
|
|
124
|
+
}
|
|
105
125
|
|
|
106
|
-
|
|
126
|
+
// Trust createTx for both amount and fee. For native sweep `amount` is
|
|
127
|
+
// the post-subtraction value (input balance minus accurate fee). For
|
|
128
|
+
// token sweep `amount` is the input token balance unchanged, since we
|
|
129
|
+
// don't pass `amountIncludesFee`. In both cases `fee` includes the
|
|
130
|
+
// OP-stack L1 data fee from `calculateFee`, which matters more for
|
|
131
|
+
// tokens than for native because ERC-20 calldata is larger.
|
|
132
|
+
return {
|
|
133
|
+
fromAddress,
|
|
134
|
+
toAddress,
|
|
135
|
+
amount: createTxResult.amount,
|
|
136
|
+
fee: createTxResult.fee,
|
|
137
|
+
privateKey,
|
|
138
|
+
unsignedTx: createTxResult.unsignedTx,
|
|
139
|
+
}
|
|
107
140
|
}
|
|
108
141
|
|
|
109
142
|
const sendFunds = async ({ privateKey, unsignedTx }) => {
|
package/src/send-validations.js
CHANGED
|
@@ -39,14 +39,13 @@ export const createSendValidations = ({ assetClientInterface, checkTx }) => {
|
|
|
39
39
|
})
|
|
40
40
|
if (!fromAddress) return
|
|
41
41
|
|
|
42
|
-
//
|
|
42
|
+
// Native send: empty calldata, no signed tx hash to pass yet.
|
|
43
43
|
const result = await checkTx({
|
|
44
44
|
chain: asset.chainId,
|
|
45
45
|
fromAddress,
|
|
46
46
|
toAddress: destinationAddress,
|
|
47
47
|
value: sendAmount.toBaseString({ unit: false }),
|
|
48
48
|
input: '0x',
|
|
49
|
-
hash: '0x1',
|
|
50
49
|
})
|
|
51
50
|
|
|
52
51
|
if (result?.action !== 'WARN') return
|
package/src/tx-create.js
CHANGED
|
@@ -20,6 +20,62 @@ import {
|
|
|
20
20
|
TX_TYPE_TRANSFER,
|
|
21
21
|
} from './tx-type/index.js'
|
|
22
22
|
|
|
23
|
+
// Thrown by `resolveAmountIncludesFeeTxAttributes` when the fee subtraction
|
|
24
|
+
// implied by `amountIncludesFee` would drive the resulting tx value below
|
|
25
|
+
// zero (i.e. the caller's balance can't cover the computed fee). Exported so
|
|
26
|
+
// callers like MoveFunds can `instanceof`-check and translate it into their
|
|
27
|
+
// own user-facing error type without string-matching the message.
|
|
28
|
+
export class AmountIncludesFeeUnderflowError extends Error {
|
|
29
|
+
constructor() {
|
|
30
|
+
super('transaction gas cost exceeds fee-inclusive amount')
|
|
31
|
+
this.name = 'AmountIncludesFeeUnderflowError'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function calculateFee({
|
|
36
|
+
asset,
|
|
37
|
+
chainId,
|
|
38
|
+
eip1559Enabled,
|
|
39
|
+
gasLimit,
|
|
40
|
+
gasPrice,
|
|
41
|
+
tipGasPrice,
|
|
42
|
+
nonce,
|
|
43
|
+
txInput,
|
|
44
|
+
txToAddress,
|
|
45
|
+
txValue,
|
|
46
|
+
}) {
|
|
47
|
+
const ethjsTx = createEthereumJsTx({
|
|
48
|
+
txData: {
|
|
49
|
+
nonce,
|
|
50
|
+
gasPrice: currency2buffer(gasPrice),
|
|
51
|
+
tipGasPrice: tipGasPrice ? currency2buffer(tipGasPrice) : undefined,
|
|
52
|
+
gasLimit,
|
|
53
|
+
to: txToAddress,
|
|
54
|
+
value: currency2buffer(txValue),
|
|
55
|
+
data: txInput,
|
|
56
|
+
chainId,
|
|
57
|
+
},
|
|
58
|
+
txMeta: {
|
|
59
|
+
eip1559Enabled,
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
const transactionBuffer = ethjsTx.serialize()
|
|
63
|
+
|
|
64
|
+
const baseFee = gasPrice.mul(gasLimit)
|
|
65
|
+
const optimismL1DataFee = asset.baseAsset.estimateL1DataFee
|
|
66
|
+
? await asset.baseAsset.estimateL1DataFee({
|
|
67
|
+
unsignedTx: { txData: { transactionBuffer, chainId } },
|
|
68
|
+
})
|
|
69
|
+
: undefined
|
|
70
|
+
|
|
71
|
+
const l1DataFee = optimismL1DataFee
|
|
72
|
+
? asset.baseAsset.currency.baseUnit(optimismL1DataFee)
|
|
73
|
+
: asset.baseAsset.currency.ZERO
|
|
74
|
+
|
|
75
|
+
const fee = baseFee.add(l1DataFee)
|
|
76
|
+
return { fee, transactionBuffer }
|
|
77
|
+
}
|
|
78
|
+
|
|
23
79
|
async function createUnsignedTxWithFees({
|
|
24
80
|
asset,
|
|
25
81
|
chainId,
|
|
@@ -57,35 +113,19 @@ async function createUnsignedTxWithFees({
|
|
|
57
113
|
isContractTxToAddress,
|
|
58
114
|
})
|
|
59
115
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
txMeta: {
|
|
72
|
-
eip1559Enabled,
|
|
73
|
-
},
|
|
116
|
+
const { fee, transactionBuffer } = await calculateFee({
|
|
117
|
+
asset,
|
|
118
|
+
chainId,
|
|
119
|
+
eip1559Enabled,
|
|
120
|
+
gasLimit,
|
|
121
|
+
gasPrice,
|
|
122
|
+
tipGasPrice,
|
|
123
|
+
nonce,
|
|
124
|
+
txInput,
|
|
125
|
+
txToAddress,
|
|
126
|
+
txValue,
|
|
74
127
|
})
|
|
75
|
-
const transactionBuffer = ethjsTx.serialize()
|
|
76
128
|
|
|
77
|
-
const baseFee = gasPrice.mul(gasLimit)
|
|
78
|
-
const optimismL1DataFee = asset.baseAsset.estimateL1DataFee
|
|
79
|
-
? await asset.baseAsset.estimateL1DataFee({
|
|
80
|
-
unsignedTx: { txData: { transactionBuffer, chainId } },
|
|
81
|
-
})
|
|
82
|
-
: undefined
|
|
83
|
-
|
|
84
|
-
const l1DataFee = optimismL1DataFee
|
|
85
|
-
? asset.baseAsset.currency.baseUnit(optimismL1DataFee)
|
|
86
|
-
: asset.baseAsset.currency.ZERO
|
|
87
|
-
|
|
88
|
-
const fee = baseFee.add(l1DataFee)
|
|
89
129
|
const extraFeeData = getExtraFeeData({ asset, amount, txValue })
|
|
90
130
|
const unsignedTx = {
|
|
91
131
|
txData: { transactionBuffer, chainId },
|
|
@@ -102,6 +142,7 @@ async function createUnsignedTxWithFees({
|
|
|
102
142
|
return {
|
|
103
143
|
unsignedTx,
|
|
104
144
|
fee,
|
|
145
|
+
amount,
|
|
105
146
|
extraFeeData,
|
|
106
147
|
// exhcange compatibility until the use usignedTx, remove me!
|
|
107
148
|
gasPrice,
|
|
@@ -110,6 +151,60 @@ async function createUnsignedTxWithFees({
|
|
|
110
151
|
}
|
|
111
152
|
}
|
|
112
153
|
|
|
154
|
+
// TODO: Move this into tx attribute resolution once gasLimit is resolved there.
|
|
155
|
+
// `amountIncludesFee` changes the tx value, so the fee subtraction should happen
|
|
156
|
+
// in the same flow that finalizes amount, txValue, txInput, txToAddress, and
|
|
157
|
+
// gasLimit.
|
|
158
|
+
async function resolveAmountIncludesFeeTxAttributes({
|
|
159
|
+
amountIncludesFee,
|
|
160
|
+
asset,
|
|
161
|
+
chainId,
|
|
162
|
+
eip1559Enabled,
|
|
163
|
+
gasLimit,
|
|
164
|
+
gasPrice,
|
|
165
|
+
tipGasPrice,
|
|
166
|
+
txAttributes,
|
|
167
|
+
}) {
|
|
168
|
+
const { amount, nonce, txInput, txToAddress, txValue } = txAttributes
|
|
169
|
+
|
|
170
|
+
// `amountIncludesFee` is a hint: subtract any fee that is paid in the same
|
|
171
|
+
// currency as `amount`. On EVM today, gas is paid in the native asset, so
|
|
172
|
+
// for ERC-20 sends and zero-value calldata (i.e approve()) flows there is nothing in the
|
|
173
|
+
// amount's currency to subtract. Pass through silently in those cases, so
|
|
174
|
+
// consumers can set this flag based on intent without needing to know
|
|
175
|
+
// whether the asset is a token or whether the call carries zero native
|
|
176
|
+
// value.
|
|
177
|
+
if (!amountIncludesFee || isEthereumLikeToken(asset) || amount.isZero) {
|
|
178
|
+
return { txAttributes }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const { fee } = await calculateFee({
|
|
182
|
+
asset,
|
|
183
|
+
chainId,
|
|
184
|
+
eip1559Enabled,
|
|
185
|
+
gasLimit,
|
|
186
|
+
gasPrice,
|
|
187
|
+
tipGasPrice,
|
|
188
|
+
nonce,
|
|
189
|
+
txInput,
|
|
190
|
+
txToAddress,
|
|
191
|
+
txValue,
|
|
192
|
+
})
|
|
193
|
+
const reducedAmount = amount.sub(fee)
|
|
194
|
+
|
|
195
|
+
if (!reducedAmount.gte(asset.currency.ZERO)) {
|
|
196
|
+
throw new AmountIncludesFeeUnderflowError()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
txAttributes: {
|
|
201
|
+
...txAttributes,
|
|
202
|
+
amount: reducedAmount,
|
|
203
|
+
txValue: reducedAmount,
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
113
208
|
const resolveTxFactoryGasPrices = async ({
|
|
114
209
|
assetClientInterface,
|
|
115
210
|
baseAsset,
|
|
@@ -384,7 +479,12 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
384
479
|
gasPrice: providedGasPrice,
|
|
385
480
|
bip70,
|
|
386
481
|
customFee: providedCustomFee,
|
|
482
|
+
// Intent flag: the user is trying to empty the wallet. Used for send-all-only
|
|
483
|
+
// dust behavior such as the EIP-1559 tip override.
|
|
387
484
|
isSendAll,
|
|
485
|
+
// Amount semantics flag: the provided amount includes the fee budget, so the
|
|
486
|
+
// final tx value should be reduced by the computed fee.
|
|
487
|
+
amountIncludesFee = false,
|
|
388
488
|
bumpTxId,
|
|
389
489
|
}) => {
|
|
390
490
|
assert(asset, 'asset is required')
|
|
@@ -520,6 +620,7 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
520
620
|
bip70,
|
|
521
621
|
amount: resolvedTxAttributes.amount,
|
|
522
622
|
txType,
|
|
623
|
+
isSendAll,
|
|
523
624
|
}))
|
|
524
625
|
|
|
525
626
|
// HACK: We cannot ensure the no dust invariant for `isSendAll`
|
|
@@ -532,8 +633,21 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
532
633
|
const isSendAllBaseAsset =
|
|
533
634
|
isSendAll && !isEthereumLikeToken(asset) && !resolvedTxAttributes.isContractTxToAddress
|
|
534
635
|
|
|
636
|
+
const finalTipGasPrice = isSendAllBaseAsset && eip1559Enabled ? gasPrice : tipGasPrice
|
|
637
|
+
|
|
638
|
+
const { txAttributes: finalTxAttributes } = await resolveAmountIncludesFeeTxAttributes({
|
|
639
|
+
amountIncludesFee,
|
|
640
|
+
asset,
|
|
641
|
+
chainId,
|
|
642
|
+
eip1559Enabled,
|
|
643
|
+
gasLimit,
|
|
644
|
+
gasPrice,
|
|
645
|
+
tipGasPrice: finalTipGasPrice,
|
|
646
|
+
txAttributes: resolvedTxAttributes,
|
|
647
|
+
})
|
|
648
|
+
|
|
535
649
|
return createUnsignedTxWithFees({
|
|
536
|
-
...
|
|
650
|
+
...finalTxAttributes,
|
|
537
651
|
asset,
|
|
538
652
|
chainId,
|
|
539
653
|
gasLimit,
|
|
@@ -545,7 +659,7 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
545
659
|
// fixed gas cost transaction, no dust balance should remain,
|
|
546
660
|
// since any deviation in the underlying `baseFeePerGas` will
|
|
547
661
|
// result only affect the tip for the miner - no dust remains.
|
|
548
|
-
tipGasPrice:
|
|
662
|
+
tipGasPrice: finalTipGasPrice,
|
|
549
663
|
eip1559Enabled,
|
|
550
664
|
})
|
|
551
665
|
}
|