@exodus/ethereum-api 8.36.0 → 8.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/package.json +2 -4
- package/src/create-asset-utils.js +76 -0
- package/src/create-asset.js +26 -27
- package/src/exodus-eth-server/clarity-v2.js +40 -19
- package/src/gas-estimation.js +96 -44
- package/src/get-balances.js +3 -1
- package/src/get-fee-async.js +9 -61
- package/src/get-fee.js +1 -1
- package/src/index.js +1 -1
- package/src/staking/ethereum/service.js +4 -2
- package/src/tx-send/get-fee-info.js +1 -0
- package/src/tx-send/nonce-utils.js +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [8.38.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.37.0...@exodus/ethereum-api@8.38.0) (2025-06-17)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat: clarity basemainnet (#5876)
|
|
13
|
+
|
|
14
|
+
* feat: enable evm transaction privacy (#5869)
|
|
15
|
+
|
|
16
|
+
* feat: remove eth sepolia and goerli (#5885)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## [8.37.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.36.0...@exodus/ethereum-api@8.37.0) (2025-06-12)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
* feat: remote config feeData.gasLimits (#5830)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
6
30
|
## [8.36.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.35.1...@exodus/ethereum-api@8.36.0) (2025-06-11)
|
|
7
31
|
|
|
8
32
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.38.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",
|
|
@@ -52,8 +52,6 @@
|
|
|
52
52
|
"@exodus/assets-testing": "^1.0.0",
|
|
53
53
|
"@exodus/bsc-meta": "^2.1.2",
|
|
54
54
|
"@exodus/ethereumarbone-meta": "^2.0.3",
|
|
55
|
-
"@exodus/ethereumgoerli-meta": "^2.0.1",
|
|
56
|
-
"@exodus/ethereumsepolia-meta": "^2.0.1",
|
|
57
55
|
"@exodus/fantommainnet-meta": "^2.0.2",
|
|
58
56
|
"@exodus/rootstock-meta": "^2.0.3"
|
|
59
57
|
},
|
|
@@ -64,5 +62,5 @@
|
|
|
64
62
|
"type": "git",
|
|
65
63
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
66
64
|
},
|
|
67
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "7150173679bc6f43d40c151728ae5f910bc52d88"
|
|
68
66
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
|
|
3
|
+
import { createEvmServer, ValidMonitorTypes } from './exodus-eth-server/index.js'
|
|
4
|
+
|
|
5
|
+
// Determines the appropriate `monitorType`, `serverUrl` and `monitorInterval`
|
|
6
|
+
// to use for a given config.
|
|
7
|
+
export const resolveMonitorSettings = (
|
|
8
|
+
{
|
|
9
|
+
asset,
|
|
10
|
+
configWithOverrides,
|
|
11
|
+
defaultMonitorInterval,
|
|
12
|
+
defaultMonitorType,
|
|
13
|
+
defaultServerUrl,
|
|
14
|
+
} = Object.create(null)
|
|
15
|
+
) => {
|
|
16
|
+
assert(asset, 'expected asset')
|
|
17
|
+
assert(
|
|
18
|
+
ValidMonitorTypes.includes(defaultMonitorType),
|
|
19
|
+
`defaultMonitorType ${defaultMonitorType} for ${asset.name} is invalid.`
|
|
20
|
+
)
|
|
21
|
+
assert(defaultServerUrl, `expected default serverUrl for ${asset.name}`)
|
|
22
|
+
|
|
23
|
+
const overrideMonitorType = configWithOverrides?.monitorType
|
|
24
|
+
const overrideServerUrl = configWithOverrides?.serverUrl
|
|
25
|
+
const overrideMonitorInterval = configWithOverrides?.monitorInterval
|
|
26
|
+
|
|
27
|
+
const defaultResolution = {
|
|
28
|
+
// NOTE: Regardless of the `monitorType`, the `monitorInterval`
|
|
29
|
+
// will be respected.
|
|
30
|
+
monitorInterval: overrideMonitorInterval ?? defaultMonitorInterval,
|
|
31
|
+
monitorType: defaultMonitorType,
|
|
32
|
+
serverUrl: defaultServerUrl,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!overrideMonitorType && overrideServerUrl) {
|
|
36
|
+
console.warn(
|
|
37
|
+
`Received an \`overrideServerUrl\`, but not the \`monitorType\` for ${asset.name}. Falling back to ${defaultMonitorType}<${defaultServerUrl}>.`
|
|
38
|
+
)
|
|
39
|
+
return defaultResolution
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If we don't attempt to override the `monitorType`, we resort
|
|
43
|
+
// to the default configuration.
|
|
44
|
+
if (!overrideMonitorType) return defaultResolution
|
|
45
|
+
|
|
46
|
+
if (!ValidMonitorTypes.includes(overrideMonitorType)) {
|
|
47
|
+
console.warn(
|
|
48
|
+
`"${overrideMonitorType}" is not a valid \`MonitorType\`. Falling back to ${defaultMonitorType}<${defaultServerUrl}>.`
|
|
49
|
+
)
|
|
50
|
+
return defaultResolution
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Permit the `monitorType` and `serverUrl` to be overrided.
|
|
54
|
+
return { ...defaultResolution, monitorType: overrideMonitorType, serverUrl: overrideServerUrl }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const createBroadcastTxFactory = ({ assetName, server, privacyRpcUrl }) => {
|
|
58
|
+
assert(server, 'expected server')
|
|
59
|
+
|
|
60
|
+
const defaultResult = {
|
|
61
|
+
broadcastTx: (...args) => server.sendRawTransaction(...args),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!privacyRpcUrl) return defaultResult
|
|
65
|
+
|
|
66
|
+
const privacyServer = createEvmServer({
|
|
67
|
+
assetName,
|
|
68
|
+
serverUrl: privacyRpcUrl,
|
|
69
|
+
monitorType: 'no-history',
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
...defaultResult,
|
|
74
|
+
broadcastPrivateTx: (...args) => privacyServer.sendRawTransaction(...args),
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/create-asset.js
CHANGED
|
@@ -22,9 +22,10 @@ import assert from 'minimalistic-assert'
|
|
|
22
22
|
import ms from 'ms'
|
|
23
23
|
|
|
24
24
|
import { addressHasHistoryFactory } from './address-has-history.js'
|
|
25
|
+
import { createBroadcastTxFactory, resolveMonitorSettings } from './create-asset-utils.js'
|
|
25
26
|
import { createTokenFactory } from './create-token-factory.js'
|
|
26
27
|
import { createCustomFeesApi } from './custom-fees.js'
|
|
27
|
-
import { createEvmServer
|
|
28
|
+
import { createEvmServer } from './exodus-eth-server/index.js'
|
|
28
29
|
import { createFeeData } from './fee-data-factory.js'
|
|
29
30
|
import { createGetBalanceForAddress } from './get-balance-for-address.js'
|
|
30
31
|
import { getBalancesFactory } from './get-balances.js'
|
|
@@ -57,10 +58,10 @@ export const createAssetFactory = ({
|
|
|
57
58
|
isMaxFeeAsset = false,
|
|
58
59
|
isTestnet = false,
|
|
59
60
|
l1GasOracleAddress, // l1 extra fee for base and optostakingConfiguration = {},
|
|
60
|
-
monitorInterval,
|
|
61
|
-
monitorType = 'magnifier',
|
|
61
|
+
monitorInterval: defaultMonitorInterval,
|
|
62
|
+
monitorType: defaultMonitorType = 'magnifier',
|
|
62
63
|
nfts: defaultNfts = false,
|
|
63
|
-
serverUrl,
|
|
64
|
+
serverUrl: defaultServerUrl,
|
|
64
65
|
stakingConfiguration = {},
|
|
65
66
|
useEip1191ChainIdChecksum = false,
|
|
66
67
|
forceGasLimitEstimation = false,
|
|
@@ -69,7 +70,7 @@ export const createAssetFactory = ({
|
|
|
69
70
|
}) => {
|
|
70
71
|
assert(assetsList, 'assetsList is required')
|
|
71
72
|
assert(providedFeeData || feeDataConfig, 'feeData or feeDataConfig is required')
|
|
72
|
-
assert(
|
|
73
|
+
assert(defaultServerUrl, 'serverUrl is required')
|
|
73
74
|
assert(confirmationsNumber, 'confirmationsNumber is required')
|
|
74
75
|
|
|
75
76
|
const base = assetsList.find((asset) => asset.name === asset.baseAssetName)
|
|
@@ -98,41 +99,32 @@ export const createAssetFactory = ({
|
|
|
98
99
|
) => {
|
|
99
100
|
const assets = connectAssetsList(assetsList)
|
|
100
101
|
|
|
102
|
+
const configWithOverrides = { ...defaultConfig, ...config }
|
|
103
|
+
|
|
101
104
|
const {
|
|
102
105
|
allowMetaMaskCompat,
|
|
103
106
|
supportsStaking,
|
|
104
107
|
nfts,
|
|
105
108
|
customTokens,
|
|
106
109
|
supportsCustomFees,
|
|
107
|
-
monitorType: overrideMonitorType,
|
|
108
|
-
serverUrl: overrideServerUrl,
|
|
109
|
-
monitorInterval: overrideMonitorInterval,
|
|
110
110
|
useAbsoluteBalanceAndNonce: overrideUseAbsoluteBalanceAndNonce,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
...config,
|
|
114
|
-
}
|
|
111
|
+
privacyRpcUrl,
|
|
112
|
+
} = configWithOverrides
|
|
115
113
|
|
|
116
114
|
const asset = assets[base.name]
|
|
117
115
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
monitorType = overrideMonitorType
|
|
126
|
-
serverUrl = overrideServerUrl
|
|
127
|
-
monitorInterval = overrideMonitorInterval || monitorInterval
|
|
128
|
-
}
|
|
129
|
-
}
|
|
116
|
+
const { monitorType, serverUrl, monitorInterval } = resolveMonitorSettings({
|
|
117
|
+
asset,
|
|
118
|
+
configWithOverrides,
|
|
119
|
+
defaultMonitorInterval,
|
|
120
|
+
defaultMonitorType,
|
|
121
|
+
defaultServerUrl,
|
|
122
|
+
})
|
|
130
123
|
|
|
131
124
|
if (overrideUseAbsoluteBalanceAndNonce !== undefined) {
|
|
132
125
|
useAbsoluteBalanceAndNonce = overrideUseAbsoluteBalanceAndNonce
|
|
133
126
|
}
|
|
134
127
|
|
|
135
|
-
monitorType = overrideMonitorType || monitorType
|
|
136
128
|
const server = createEvmServer({ assetName: asset.name, serverUrl, monitorType })
|
|
137
129
|
|
|
138
130
|
const gasLimit = 21e3 // 21 KGas, enough only for sending ether to normal address
|
|
@@ -274,10 +266,15 @@ export const createAssetFactory = ({
|
|
|
274
266
|
? getL1GetFeeFactory({ asset, originalGetFee })
|
|
275
267
|
: originalGetFee
|
|
276
268
|
|
|
269
|
+
const { broadcastTx, broadcastPrivateTx } = createBroadcastTxFactory({
|
|
270
|
+
assetName: asset.name,
|
|
271
|
+
server,
|
|
272
|
+
privacyRpcUrl,
|
|
273
|
+
})
|
|
274
|
+
|
|
277
275
|
const api = {
|
|
278
|
-
getActivityTxs,
|
|
279
276
|
addressHasHistory,
|
|
280
|
-
broadcastTx
|
|
277
|
+
broadcastTx,
|
|
281
278
|
createAccountState: () => accountStateClass,
|
|
282
279
|
createFeeMonitor,
|
|
283
280
|
createHistoryMonitor,
|
|
@@ -286,6 +283,7 @@ export const createAssetFactory = ({
|
|
|
286
283
|
customFees: createCustomFeesApi({ baseAsset: asset }),
|
|
287
284
|
defaultAddressPath,
|
|
288
285
|
features,
|
|
286
|
+
getActivityTxs,
|
|
289
287
|
getBalances,
|
|
290
288
|
getBalanceForAddress: createGetBalanceForAddress({ asset, server }),
|
|
291
289
|
getConfirmationsNumber: () => confirmationsNumber,
|
|
@@ -327,6 +325,7 @@ export const createAssetFactory = ({
|
|
|
327
325
|
chainId,
|
|
328
326
|
monitorType,
|
|
329
327
|
estimateL1DataFee,
|
|
328
|
+
broadcastPrivateTx,
|
|
330
329
|
forceGasLimitEstimation,
|
|
331
330
|
server,
|
|
332
331
|
...(erc20FuelBuffer && { erc20FuelBuffer }),
|
|
@@ -2,6 +2,33 @@ import { retry } from '@exodus/simple-retry'
|
|
|
2
2
|
|
|
3
3
|
import ClarityServer from './clarity.js'
|
|
4
4
|
|
|
5
|
+
export const encodeCursor = (blockNumberBigInt, isLegacy = false) => {
|
|
6
|
+
if (typeof blockNumberBigInt !== 'bigint') throw new Error('expected bigint')
|
|
7
|
+
|
|
8
|
+
if (isLegacy) {
|
|
9
|
+
const cursor = Buffer.alloc(26)
|
|
10
|
+
cursor.writeBigInt64LE(blockNumberBigInt, 10)
|
|
11
|
+
return cursor
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const cursor = Buffer.alloc(8)
|
|
15
|
+
cursor.writeBigUInt64LE(blockNumberBigInt, 0)
|
|
16
|
+
return cursor
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const decodeCursor = (cursor) => {
|
|
20
|
+
if (Buffer.isBuffer(cursor)) {
|
|
21
|
+
// New format: buffer containing only the block number
|
|
22
|
+
if (cursor.length === 8) return { blockNumber: cursor.readBigUInt64LE(0) }
|
|
23
|
+
|
|
24
|
+
// Old format: buffer with length 26
|
|
25
|
+
if (cursor.length === 26) return { blockNumber: cursor.readBigInt64LE(10) }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Doesn't match any known format, return default
|
|
29
|
+
return { blockNumber: BigInt(0) }
|
|
30
|
+
}
|
|
31
|
+
|
|
5
32
|
const getTextFromResponse = async (response) => {
|
|
6
33
|
try {
|
|
7
34
|
const responseBody = await response.text()
|
|
@@ -44,18 +71,25 @@ export default class ClarityServerV2 extends ClarityServer {
|
|
|
44
71
|
this.updateBaseApiPath()
|
|
45
72
|
}
|
|
46
73
|
|
|
74
|
+
getTransactionsAtBlockNumber = async ({ address, blockNumber, withInput = true }) => {
|
|
75
|
+
const url = new URL(`${this.baseApiPath}/addresses/${encodeURIComponent(address)}/transactions`)
|
|
76
|
+
url.searchParams.set('cursor', blockNumber)
|
|
77
|
+
url.searchParams.set('withInput', Boolean(withInput).toString())
|
|
78
|
+
return fetchJsonRetry(url)
|
|
79
|
+
}
|
|
80
|
+
|
|
47
81
|
async getAllTransactions({ address, cursor }) {
|
|
48
|
-
let { blockNumber } =
|
|
82
|
+
let { blockNumber } = decodeCursor(cursor)
|
|
49
83
|
blockNumber = blockNumber.toString()
|
|
50
84
|
|
|
51
85
|
let transactions = []
|
|
52
86
|
|
|
53
87
|
while (true) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
88
|
+
const { transactions: txs, cursor: nextBlockNumber } =
|
|
89
|
+
await this.getTransactionsAtBlockNumber({
|
|
90
|
+
address,
|
|
91
|
+
blockNumber,
|
|
92
|
+
})
|
|
59
93
|
|
|
60
94
|
if (txs.length === 0) {
|
|
61
95
|
// fetch until no more new transactions
|
|
@@ -91,19 +125,6 @@ export default class ClarityServerV2 extends ClarityServer {
|
|
|
91
125
|
}
|
|
92
126
|
}
|
|
93
127
|
|
|
94
|
-
#decodeCursor(cursor) {
|
|
95
|
-
if (Buffer.isBuffer(cursor)) {
|
|
96
|
-
// New format: buffer containing only the block number
|
|
97
|
-
if (cursor.length === 8) return { blockNumber: cursor.readBigUInt64LE(0) }
|
|
98
|
-
|
|
99
|
-
// Old format: buffer with length 26
|
|
100
|
-
if (cursor.length === 26) return { blockNumber: cursor.readBigInt64LE(10) }
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Doesn't match any known format, return default
|
|
104
|
-
return { blockNumber: BigInt(0) }
|
|
105
|
-
}
|
|
106
|
-
|
|
107
128
|
async sendHttpRequest({ path, method, body }) {
|
|
108
129
|
const url = new URL(`${this.baseApiPath}${path}`)
|
|
109
130
|
const fetchOptions = {
|
package/src/gas-estimation.js
CHANGED
|
@@ -1,28 +1,50 @@
|
|
|
1
1
|
import { currency2buffer, isEthereumLikeToken } from '@exodus/ethereum-lib'
|
|
2
2
|
import { bufferToHex, toBuffer } from '@exodus/ethereumjs/util'
|
|
3
3
|
|
|
4
|
-
import { estimateGas, isContractAddressCached } from './eth-like-util.js'
|
|
4
|
+
import { estimateGas, isContractAddressCached, isForwarderContractCached } from './eth-like-util.js'
|
|
5
5
|
|
|
6
|
-
export const
|
|
6
|
+
export const DEFAULT_GAS_LIMIT_MULTIPLIER = 1.29
|
|
7
7
|
|
|
8
8
|
// 16 gas per non-zero byte (4 gas per zero byte) of "transaction input data"
|
|
9
9
|
const GAS_PER_NON_ZERO_BYTE = 16
|
|
10
10
|
|
|
11
11
|
export const DEFAULT_CONTRACT_GAS_LIMIT = 1e6
|
|
12
12
|
|
|
13
|
+
// HACK: If a recipient address is not defined, we usually fall back to
|
|
14
|
+
// default address so gas estimation can still complete successfully
|
|
15
|
+
// without knowledge of which accounts are involved.
|
|
16
|
+
//
|
|
17
|
+
// However, we must be careful to select addresses which are unlikely
|
|
18
|
+
// to have existing obligations, such as popular dead addresses or the
|
|
19
|
+
// reserved addresses of precompiles, since these can influence gas
|
|
20
|
+
// estimation.
|
|
21
|
+
//
|
|
22
|
+
// Here, we use an address which is mostly all `1`s to make sure we can
|
|
23
|
+
// exaggerate the worst-case calldata cost (which is priced per high bit)
|
|
24
|
+
// whilst being unlikely to have any token balances.
|
|
25
|
+
//
|
|
26
|
+
// Unfortunately, we can't use `0xffffffffffffffffffffffffffffffffffffffff`,
|
|
27
|
+
// since this address is a whale.
|
|
28
|
+
export const ARBITRARY_ADDRESS = '0xfffFfFfFfFfFFFFFfeFfFFFffFffFFFFfFFFFFFF'.toLowerCase()
|
|
29
|
+
|
|
13
30
|
// HACK: RPCs generally provide imprecise estimates
|
|
14
31
|
// for `gasUsed` (often these are insufficient).
|
|
15
|
-
export const scaleGasLimitEstimate = ({
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
export const scaleGasLimitEstimate = ({
|
|
33
|
+
estimatedGasLimit,
|
|
34
|
+
gasLimitMultiplier = DEFAULT_GAS_LIMIT_MULTIPLIER,
|
|
35
|
+
}) => {
|
|
36
|
+
const gasLimit = parseInt(estimatedGasLimit)
|
|
37
|
+
if (!Number.isInteger(gasLimit)) {
|
|
38
|
+
throw new TypeError('Invalid scaleGasLimitEstimate ' + estimatedGasLimit)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const multiplier = parseFloat(gasLimitMultiplier)
|
|
42
|
+
if (Number.isNaN(multiplier)) {
|
|
43
|
+
throw new TypeError('Invalid gasLimitMultiplier ' + gasLimitMultiplier)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Math.floor(gasLimit * multiplier)
|
|
47
|
+
}
|
|
26
48
|
|
|
27
49
|
// Starting with geth v1.9.14, if gasPrice is set for eth_estimateGas call, the call allowance will
|
|
28
50
|
// be calculated with account's balance divided by gasPrice. If user's balance is too low,
|
|
@@ -34,14 +56,8 @@ export async function estimateGasLimit(
|
|
|
34
56
|
toAddress,
|
|
35
57
|
amount,
|
|
36
58
|
data,
|
|
37
|
-
gasPrice = '0x'
|
|
38
|
-
extraPercentage
|
|
59
|
+
gasPrice = '0x'
|
|
39
60
|
) {
|
|
40
|
-
// move this to remote config
|
|
41
|
-
if (hardcodedGasLimits.has(asset.name)) {
|
|
42
|
-
return hardcodedGasLimits.get(asset.name)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
61
|
const opts = {
|
|
46
62
|
from: fromAddress,
|
|
47
63
|
to: toAddress,
|
|
@@ -51,62 +67,98 @@ export async function estimateGasLimit(
|
|
|
51
67
|
}
|
|
52
68
|
|
|
53
69
|
const estimatedGas = await estimateGas({ asset, ...opts })
|
|
70
|
+
return Number(BigInt(estimatedGas))
|
|
71
|
+
}
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
export async function resolveGasLimitMultiplier({ asset, feeData, toAddress, fromAddress }) {
|
|
74
|
+
const gasLimitMultiplierWhenUnknownAddress =
|
|
75
|
+
feeData?.gasLimits?.[asset.name]?.gasLimitMultiplierWhenUnknownAddress
|
|
76
|
+
|
|
77
|
+
if (gasLimitMultiplierWhenUnknownAddress && (!fromAddress || !toAddress)) {
|
|
78
|
+
return gasLimitMultiplierWhenUnknownAddress
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const gasLimitMultiplier = feeData?.gasLimits?.[asset.name]?.gasLimitMultiplier
|
|
82
|
+
|
|
83
|
+
if (gasLimitMultiplier) {
|
|
84
|
+
return gasLimitMultiplier
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (asset.baseAsset.estimateL1DataFee) {
|
|
88
|
+
return 2
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const isToken = asset.name !== asset.baseAsset.name
|
|
92
|
+
|
|
93
|
+
// calling forwarder contracts with a bumped gas limit causes 'Out Of Gas' error on chain
|
|
94
|
+
const hasForwarderContract =
|
|
95
|
+
!isToken && toAddress ? await isForwarderContractCached({ asset, address: toAddress }) : false
|
|
96
|
+
|
|
97
|
+
if (hasForwarderContract) {
|
|
98
|
+
return 1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return DEFAULT_GAS_LIMIT_MULTIPLIER
|
|
59
102
|
}
|
|
60
103
|
|
|
61
104
|
export function resolveDefaultTxInput({ asset, toAddress, amount }) {
|
|
62
105
|
return isEthereumLikeToken(asset)
|
|
63
|
-
? bufferToHex(asset.contract.transfer.build(toAddress.toLowerCase(), amount
|
|
106
|
+
? bufferToHex(asset.contract.transfer.build(toAddress.toLowerCase(), amount?.toBaseString()))
|
|
64
107
|
: '0x'
|
|
65
108
|
}
|
|
66
109
|
|
|
67
110
|
export async function fetchGasLimit({
|
|
68
111
|
asset,
|
|
69
|
-
|
|
70
|
-
|
|
112
|
+
feeData,
|
|
113
|
+
fromAddress: providedFromAddress,
|
|
114
|
+
toAddress: providedToAddress,
|
|
71
115
|
txInput: providedTxInput,
|
|
72
|
-
amount,
|
|
116
|
+
amount: providedAmount,
|
|
73
117
|
bip70,
|
|
74
118
|
throwOnError = true,
|
|
75
|
-
extraPercentage,
|
|
76
119
|
}) {
|
|
77
120
|
if (bip70?.bitpay?.data && bip70?.bitpay?.gasPrice)
|
|
78
121
|
return asset.name === 'ethereum' ? 65_000 : 130_000 // from on chain stats https://dune.xyz/queries/189123
|
|
79
122
|
|
|
80
|
-
|
|
123
|
+
const fixedGasLimit = feeData?.gasLimits?.[asset.name]?.fixedGasLimit
|
|
124
|
+
if (fixedGasLimit) {
|
|
125
|
+
return fixedGasLimit
|
|
126
|
+
}
|
|
81
127
|
|
|
128
|
+
const amount = providedAmount ?? asset.currency.ZERO
|
|
129
|
+
const fromAddress = providedFromAddress ?? ARBITRARY_ADDRESS
|
|
130
|
+
const toAddress = providedToAddress ?? ARBITRARY_ADDRESS
|
|
82
131
|
const txInput = providedTxInput || resolveDefaultTxInput({ asset, toAddress, amount })
|
|
83
132
|
|
|
84
133
|
const defaultGasLimit = () => asset.gasLimit + GAS_PER_NON_ZERO_BYTE * toBuffer(txInput).length
|
|
85
134
|
|
|
86
135
|
const isContract = await isContractAddressCached({ asset, address: toAddress })
|
|
87
136
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
toAddress = asset.contract.address
|
|
91
|
-
} else if (!isContract && !asset.forceGasLimitEstimation) {
|
|
137
|
+
const isToken = isEthereumLikeToken(asset)
|
|
138
|
+
if (!isToken && !isContract && !asset.forceGasLimitEstimation) {
|
|
92
139
|
return defaultGasLimit()
|
|
93
140
|
}
|
|
94
141
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
142
|
+
const gasLimitMultiplier = await resolveGasLimitMultiplier({
|
|
143
|
+
asset,
|
|
144
|
+
feeData,
|
|
145
|
+
fromAddress: providedFromAddress,
|
|
146
|
+
toAddress: providedToAddress,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const txToAddress = isToken ? asset.contract.address : toAddress
|
|
150
|
+
const txAmount = isToken ? asset.baseAsset.currency.ZERO : amount
|
|
99
151
|
|
|
100
152
|
try {
|
|
101
|
-
|
|
153
|
+
const estimatedGasLimit = await estimateGasLimit(
|
|
102
154
|
asset,
|
|
103
155
|
fromAddress,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
txInput
|
|
107
|
-
gasPrice,
|
|
108
|
-
extraPercentage
|
|
156
|
+
txToAddress,
|
|
157
|
+
txAmount,
|
|
158
|
+
txInput
|
|
109
159
|
)
|
|
160
|
+
|
|
161
|
+
return scaleGasLimitEstimate({ estimatedGasLimit, gasLimitMultiplier })
|
|
110
162
|
} catch (err) {
|
|
111
163
|
if (throwOnError) throw err
|
|
112
164
|
|
package/src/get-balances.js
CHANGED
|
@@ -18,7 +18,9 @@ export const getAbsoluteBalance = ({ asset, txLog }) => {
|
|
|
18
18
|
let balance = asset.currency.ZERO
|
|
19
19
|
let hasAbsoluteBalance = false
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
const reversedTxLog = txLog.reverse()
|
|
22
|
+
|
|
23
|
+
for (const tx of reversedTxLog) {
|
|
22
24
|
if (tx.data.balanceChange) {
|
|
23
25
|
hasAbsoluteBalance = true
|
|
24
26
|
balance = balance.add(asset.currency.baseUnit(tx.data.balanceChange.to))
|
package/src/get-fee-async.js
CHANGED
|
@@ -1,53 +1,9 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { EXTRA_PERCENTAGE, fetchGasLimit, resolveDefaultTxInput } from './gas-estimation.js'
|
|
3
|
+
import { ARBITRARY_ADDRESS, fetchGasLimit, resolveDefaultTxInput } from './gas-estimation.js'
|
|
5
4
|
import { getFeeFactory } from './get-fee.js'
|
|
6
5
|
import { getNftArguments } from './nft-utils.js'
|
|
7
6
|
|
|
8
|
-
const UNKNOWN_ADDRESS_EXTRA_PERCENTAGE = {
|
|
9
|
-
usdt_bsc_ddedf0f8: 80,
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function resolveExtraPercentage({ asset, fromAddress, toAddress }) {
|
|
13
|
-
if (asset.baseAsset.estimateL1DataFee) {
|
|
14
|
-
return 100
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const isToken = asset.name !== asset.baseAsset.name
|
|
18
|
-
|
|
19
|
-
// calling forwarder contracts with a bumped gas limit causes 'Out Of Gas' error on chain
|
|
20
|
-
const hasForwarderContract =
|
|
21
|
-
!isToken && toAddress ? await isForwarderContractCached({ asset, address: toAddress }) : false
|
|
22
|
-
|
|
23
|
-
if (hasForwarderContract) {
|
|
24
|
-
return 0
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (!fromAddress || !toAddress) {
|
|
28
|
-
return UNKNOWN_ADDRESS_EXTRA_PERCENTAGE[asset.name] ?? EXTRA_PERCENTAGE
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return EXTRA_PERCENTAGE
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// HACK: If a recipient address is not defined, we usually fall back to
|
|
35
|
-
// default address so gas estimation can still complete successfully
|
|
36
|
-
// without knowledge of which accounts are involved.
|
|
37
|
-
//
|
|
38
|
-
// However, we must be careful to select addresses which are unlikely
|
|
39
|
-
// to have existing obligations, such as popular dead addresses or the
|
|
40
|
-
// reserved addresses of precompiles, since these can influence gas
|
|
41
|
-
// estimation.
|
|
42
|
-
//
|
|
43
|
-
// Here, we use an address which is mostly all `1`s to make sure we can
|
|
44
|
-
// exaggerate the worst-case calldata cost (which is priced per high bit)
|
|
45
|
-
// whilst being unlikely to have any token balances.
|
|
46
|
-
//
|
|
47
|
-
// Unfortunately, we can't use `0xffffffffffffffffffffffffffffffffffffffff`,
|
|
48
|
-
// since this address is a whale.
|
|
49
|
-
const ARBITRARY_ADDRESS = '0xfffFfFfFfFfFFFFFfeFfFFFffFffFFFFfFFFFFFF'.toLowerCase()
|
|
50
|
-
|
|
51
7
|
const getFeeAsyncFactory = ({
|
|
52
8
|
assetClientInterface,
|
|
53
9
|
gasLimit: defaultGasLimit,
|
|
@@ -59,42 +15,35 @@ const getFeeAsyncFactory = ({
|
|
|
59
15
|
return async ({
|
|
60
16
|
nft,
|
|
61
17
|
asset,
|
|
18
|
+
// provided are values from the UI or other services, they could be undefined
|
|
62
19
|
fromAddress: providedFromAddress,
|
|
63
20
|
toAddress: providedToAddress,
|
|
64
|
-
amount,
|
|
21
|
+
amount: providedAmount,
|
|
22
|
+
txInput: providedTxInput,
|
|
23
|
+
gasLimit: providedGasLimit,
|
|
65
24
|
bip70,
|
|
66
|
-
txInput: txInputPram,
|
|
67
|
-
isExchange,
|
|
68
25
|
customFee,
|
|
69
|
-
gasLimit: providedGasLimit,
|
|
70
26
|
feeData,
|
|
71
27
|
}) => {
|
|
72
28
|
const fromAddress = providedFromAddress || ARBITRARY_ADDRESS // sending from a random address
|
|
73
29
|
const toAddress = providedToAddress || ARBITRARY_ADDRESS // sending to a random address,
|
|
74
|
-
|
|
30
|
+
const amount = providedAmount ?? asset.currency.ZERO
|
|
75
31
|
const resolveGasLimit = async () => {
|
|
76
32
|
if (nft) {
|
|
77
33
|
return getNftArguments({ asset, nft, fromAddress, toAddress })
|
|
78
34
|
}
|
|
79
35
|
|
|
80
|
-
|
|
81
|
-
const txInput = txInputPram || resolveDefaultTxInput({ asset, toAddress, amount })
|
|
36
|
+
const txInput = providedTxInput || resolveDefaultTxInput({ asset, toAddress, amount })
|
|
82
37
|
if (providedGasLimit) return { gasLimit: providedGasLimit, txInput }
|
|
83
38
|
|
|
84
|
-
const
|
|
39
|
+
const gasLimit = await fetchGasLimit({
|
|
85
40
|
asset,
|
|
86
41
|
fromAddress: providedFromAddress,
|
|
87
42
|
toAddress: providedToAddress,
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
const gasLimit = await fetchGasLimit({
|
|
91
|
-
asset,
|
|
92
|
-
fromAddress,
|
|
93
|
-
toAddress,
|
|
94
43
|
txInput,
|
|
95
44
|
amount,
|
|
96
45
|
bip70,
|
|
97
|
-
|
|
46
|
+
feeData,
|
|
98
47
|
})
|
|
99
48
|
return { gasLimit, txInput }
|
|
100
49
|
}
|
|
@@ -105,7 +54,6 @@ const getFeeAsyncFactory = ({
|
|
|
105
54
|
asset,
|
|
106
55
|
feeData,
|
|
107
56
|
gasLimit,
|
|
108
|
-
isExchange,
|
|
109
57
|
amount,
|
|
110
58
|
customFee,
|
|
111
59
|
})
|
package/src/get-fee.js
CHANGED
|
@@ -41,7 +41,7 @@ export const getFeeFactoryGasPrices = ({ customFee, feeData }) => {
|
|
|
41
41
|
|
|
42
42
|
export const getFeeFactory =
|
|
43
43
|
({ gasLimit: defaultGasLimit }) =>
|
|
44
|
-
({ asset, feeData, customFee, gasLimit: providedGasLimit,
|
|
44
|
+
({ asset, feeData, customFee, gasLimit: providedGasLimit, amount }) => {
|
|
45
45
|
const {
|
|
46
46
|
feeData: { tipGasPrice, eip1559Enabled },
|
|
47
47
|
gasPrice,
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { estimateGasLimit } from '../../gas-estimation.js'
|
|
1
|
+
import { estimateGasLimit, scaleGasLimitEstimate } from '../../gas-estimation.js'
|
|
2
2
|
import { createWatchTx as defaultCreateWatch } from '../../watch-tx.js'
|
|
3
3
|
import { stakingProviderClientFactory } from '../staking-provider-client.js'
|
|
4
4
|
import { amountToCurrency, DISABLE_BALANCE_CHECKS, resolveFeeData } from '../utils/index.js'
|
|
@@ -368,8 +368,10 @@ export function createEthereumStakingService({
|
|
|
368
368
|
DISABLE_BALANCE_CHECKS
|
|
369
369
|
)
|
|
370
370
|
|
|
371
|
+
const scaledGasLimit = scaleGasLimitEstimate({ estimatedGasLimit })
|
|
372
|
+
|
|
371
373
|
const gasLimit = Math.max(
|
|
372
|
-
|
|
374
|
+
scaledGasLimit + EXTRA_GAS_LIMIT,
|
|
373
375
|
|
|
374
376
|
// For delgation transactions, we enforce an empirical
|
|
375
377
|
// `MINIMUM_DELEGATE_GAS_LIMIT`, since the majority of
|
|
@@ -28,9 +28,11 @@ export const resolveNonce = async ({
|
|
|
28
28
|
export const getNonceFromTxLog = ({ txLog, useAbsoluteNonce }) => {
|
|
29
29
|
let absoluteNonce = 0
|
|
30
30
|
if (useAbsoluteNonce) {
|
|
31
|
-
|
|
31
|
+
const reversedTxLog = txLog.reverse()
|
|
32
|
+
for (const tx of reversedTxLog) {
|
|
32
33
|
if (tx.data.nonceChange) {
|
|
33
34
|
absoluteNonce = parseInt(tx.data.nonceChange.to, 10)
|
|
35
|
+
break
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
}
|