@exodus/ethereum-api 8.35.2-alpha.0 → 8.37.0-patch.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 CHANGED
@@ -3,36 +3,31 @@
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.35.2-alpha.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.35.1...@exodus/ethereum-api@8.35.2-alpha.0) (2025-06-11)
6
+ ## [8.37.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.36.0...@exodus/ethereum-api@8.37.0) (2025-06-12)
7
7
 
8
8
 
9
9
  ### Features
10
10
 
11
- * add clarity monitor v2 ([1e08a69](https://github.com/ExodusMovement/assets/commit/1e08a694026acd3f68fc9a154b2d3da99044ae95))
12
- * add ws client for ws-gateway [WIP] ([05bf8b6](https://github.com/ExodusMovement/assets/commit/05bf8b616809b955fa3face60a7db2dd7b8a6259))
13
- * add ws-gateway client ([12b6178](https://github.com/ExodusMovement/assets/commit/12b6178a113ad68063532fe12ffded4aa47ff188))
14
- * improve default uri and passing ws gateway via DI ([a6cda37](https://github.com/ExodusMovement/assets/commit/a6cda37cabb0986d17bca48d4ece1792a83989ad))
15
- * improve default uri and passing ws gateway via DI ([79a37cf](https://github.com/ExodusMovement/assets/commit/79a37cf0dc75a2d4ae527e63f4ca45e533020a7c))
16
- * make unsubscribeNetwork public ([4d6b675](https://github.com/ExodusMovement/assets/commit/4d6b675c8c34fa331d256854be6f38da03aaca27))
17
- * pass logger inside ws gateway ([08f2383](https://github.com/ExodusMovement/assets/commit/08f2383f74b33cc8f940e39f1e597fac447f16c5))
18
- * remove error log, logging only reconnection ([bbaffe4](https://github.com/ExodusMovement/assets/commit/bbaffe492b5155a254a05409fd58535cb60c4cd6))
19
- * update addSingleTx after merge conflicts ([ca3128a](https://github.com/ExodusMovement/assets/commit/ca3128aba451e5a7c77bcfdb2f91c2ff009ec412))
20
- * update code after rebase ([d8b2e25](https://github.com/ExodusMovement/assets/commit/d8b2e252266f3d818147b7623ed762ef8cac2fdc))
21
- * update logic ([427f84d](https://github.com/ExodusMovement/assets/commit/427f84d224925e3f94c246bd26f2b14ee64442ca))
22
- * update message schema ([155814d](https://github.com/ExodusMovement/assets/commit/155814d744798d9796c315cabd262b46a4466e0c))
23
- * update subscription logic to handle edge soft-lock case ([359faa3](https://github.com/ExodusMovement/assets/commit/359faa34aa5abf7cce767c60e2ad2fdeafd9d591))
24
- * update unsubscribe logic ([fc7e288](https://github.com/ExodusMovement/assets/commit/fc7e288deb9b4815bbafd9d8c156f7cc2919bc9b))
25
- * ws-gateway dispose from network. ([7140e83](https://github.com/ExodusMovement/assets/commit/7140e83e863981fe7d79b8234d254d728cd22f4b))
26
-
27
-
28
- ### Bug Fixes
29
-
30
- * add assetName to use in tick() ([66657ca](https://github.com/ExodusMovement/assets/commit/66657cac5e184f8b6218cc282c6f51bbad25f5b5))
31
- * ethereum api cleaning up ([#5832](https://github.com/ExodusMovement/assets/issues/5832)) ([29d167e](https://github.com/ExodusMovement/assets/commit/29d167e573993be4f49ea87523f5a2156ce7cef1))
32
- * improve evm gas estimation when using arbitrary addresses ([#5842](https://github.com/ExodusMovement/assets/issues/5842)) ([9518cc1](https://github.com/ExodusMovement/assets/commit/9518cc1f6f5d861edc7a9e9fd563ee556901d5e5))
33
- * reconnection issues ([5cbdbfc](https://github.com/ExodusMovement/assets/commit/5cbdbfc8a1109c8c4e95e9b93c4b6269037e72b0))
34
- * remove extra async. Use address ([10a1380](https://github.com/ExodusMovement/assets/commit/10a13803d4fd913cf98dccc130eb22021a72245d))
35
- * update code and tests to support send method instead of emit ([379406a](https://github.com/ExodusMovement/assets/commit/379406a020f8c56048b2373e8ccb2a284ea61a5d))
11
+
12
+ * feat: remote config feeData.gasLimits (#5830)
13
+
14
+
15
+
16
+ ## [8.36.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.35.1...@exodus/ethereum-api@8.36.0) (2025-06-11)
17
+
18
+
19
+ ### Features
20
+
21
+
22
+ * feat: ethereum api use balances and nonces data (#5727)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+
28
+ * fix: ethereum api cleaning up (#5832)
29
+
30
+ * fix: improve evm gas estimation when using arbitrary addresses (#5842)
36
31
 
37
32
 
38
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.35.2-alpha.0",
3
+ "version": "8.37.0-patch.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",
@@ -28,18 +28,17 @@
28
28
  "@exodus/bip44-constants": "^195.0.0",
29
29
  "@exodus/crypto": "^1.0.0-rc.13",
30
30
  "@exodus/currency": "^6.0.1",
31
- "@exodus/ethereum-lib": "^5.10.2",
31
+ "@exodus/ethereum-lib": "^5.12.0",
32
32
  "@exodus/ethereum-meta": "^2.5.0",
33
33
  "@exodus/ethereumholesky-meta": "^2.0.2",
34
34
  "@exodus/ethereumjs": "^1.0.0",
35
35
  "@exodus/fetch": "^1.3.0",
36
- "@exodus/models": "^12.0.1",
36
+ "@exodus/models": "^12.13.0",
37
37
  "@exodus/simple-retry": "^0.0.6",
38
38
  "@exodus/solidity-contract": "^1.1.3",
39
39
  "@exodus/web3-ethereum-utils": "^4.2.1",
40
40
  "bn.js": "^5.2.1",
41
41
  "delay": "^4.0.1",
42
- "eventemitter3": "^4.0.7",
43
42
  "events": "^1.1.1",
44
43
  "idna-uts46-hx": "^2.3.1",
45
44
  "lodash": "^4.17.15",
@@ -50,7 +49,7 @@
50
49
  "ws": "^6.1.0"
51
50
  },
52
51
  "devDependencies": {
53
- "@exodus/assets-testing": "^1.0.0",
52
+ "@exodus/assets-testing": "workspace:^",
54
53
  "@exodus/bsc-meta": "^2.1.2",
55
54
  "@exodus/ethereumarbone-meta": "^2.0.3",
56
55
  "@exodus/ethereumgoerli-meta": "^2.0.1",
@@ -64,6 +63,5 @@
64
63
  "repository": {
65
64
  "type": "git",
66
65
  "url": "git+https://github.com/ExodusMovement/assets.git"
67
- },
68
- "gitHead": "d88b71e86cdc0c3289f8ddaac4e0e1b642da04f2"
66
+ }
69
67
  }
@@ -0,0 +1,55 @@
1
+ import assert from 'assert'
2
+
3
+ import { 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
+ }
@@ -22,10 +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 { 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, ValidMonitorTypes } from './exodus-eth-server/index.js'
28
- import { createWsGateway } from './exodus-eth-server/ws-gateway.js'
28
+ import { createEvmServer } from './exodus-eth-server/index.js'
29
29
  import { createFeeData } from './fee-data-factory.js'
30
30
  import { createGetBalanceForAddress } from './get-balance-for-address.js'
31
31
  import { getBalancesFactory } from './get-balances.js'
@@ -36,7 +36,6 @@ import { estimateL1DataFeeFactory, getL1GetFeeFactory } from './optimism-gas/ind
36
36
  import { serverBasedFeeMonitorFactoryFactory } from './server-based-fee-monitor.js'
37
37
  import { createStakingApi } from './staking-api.js'
38
38
  import { ClarityMonitor } from './tx-log/clarity-monitor.js'
39
- import { ClarityMonitorV2 } from './tx-log/clarity-monitor-v2.js'
40
39
  import { EthereumMonitor } from './tx-log/ethereum-monitor.js'
41
40
  import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor.js'
42
41
  import { txSendFactory } from './tx-send/index.js'
@@ -59,18 +58,19 @@ export const createAssetFactory = ({
59
58
  isMaxFeeAsset = false,
60
59
  isTestnet = false,
61
60
  l1GasOracleAddress, // l1 extra fee for base and optostakingConfiguration = {},
62
- monitorInterval,
63
- monitorType = 'magnifier',
61
+ monitorInterval: defaultMonitorInterval,
62
+ monitorType: defaultMonitorType = 'magnifier',
64
63
  nfts: defaultNfts = false,
65
- serverUrl,
64
+ serverUrl: defaultServerUrl,
66
65
  stakingConfiguration = {},
67
66
  useEip1191ChainIdChecksum = false,
68
67
  forceGasLimitEstimation = false,
69
68
  supportsCustomFees: defaultSupportsCustomFees = false,
69
+ useAbsoluteBalanceAndNonce = false,
70
70
  }) => {
71
71
  assert(assetsList, 'assetsList is required')
72
72
  assert(providedFeeData || feeDataConfig, 'feeData or feeDataConfig is required')
73
- assert(serverUrl, 'serverUrl is required')
73
+ assert(defaultServerUrl, 'serverUrl is required')
74
74
  assert(confirmationsNumber, 'confirmationsNumber is required')
75
75
 
76
76
  const base = assetsList.find((asset) => asset.name === asset.baseAssetName)
@@ -99,38 +99,32 @@ export const createAssetFactory = ({
99
99
  ) => {
100
100
  const assets = connectAssetsList(assetsList)
101
101
 
102
+ const configWithOverrides = { ...defaultConfig, ...config }
103
+
102
104
  const {
103
105
  allowMetaMaskCompat,
104
106
  supportsStaking,
105
107
  nfts,
106
108
  customTokens,
107
109
  supportsCustomFees,
108
- monitorType: overrideMonitorType,
109
- serverUrl: overrideServerUrl,
110
- monitorInterval: overrideMonitorInterval,
111
- } = {
112
- ...defaultConfig,
113
- ...config,
114
- }
110
+ useAbsoluteBalanceAndNonce: overrideUseAbsoluteBalanceAndNonce,
111
+ } = configWithOverrides
115
112
 
116
113
  const asset = assets[base.name]
117
114
 
118
- if (overrideMonitorType) {
119
- if (!ValidMonitorTypes.includes(overrideMonitorType) || !overrideServerUrl) {
120
- console.warn('Invalid config for overrideMonitorType or overrideServerUrl', {
121
- monitorType: overrideMonitorType,
122
- serverUrl: overrideServerUrl,
123
- })
124
- } else {
125
- monitorType = overrideMonitorType
126
- serverUrl = overrideServerUrl
127
- monitorInterval = overrideMonitorInterval || monitorInterval
128
- }
115
+ const { monitorType, serverUrl, monitorInterval } = resolveMonitorSettings({
116
+ asset,
117
+ configWithOverrides,
118
+ defaultMonitorInterval,
119
+ defaultMonitorType,
120
+ defaultServerUrl,
121
+ })
122
+
123
+ if (overrideUseAbsoluteBalanceAndNonce !== undefined) {
124
+ useAbsoluteBalanceAndNonce = overrideUseAbsoluteBalanceAndNonce
129
125
  }
130
126
 
131
- monitorType = overrideMonitorType || monitorType
132
127
  const server = createEvmServer({ assetName: asset.name, serverUrl, monitorType })
133
- const wsGatewayClient = createWsGateway()
134
128
 
135
129
  const gasLimit = 21e3 // 21 KGas, enough only for sending ether to normal address
136
130
 
@@ -152,6 +146,7 @@ export const createAssetFactory = ({
152
146
  const getBalances = getBalancesFactory({
153
147
  monitorType,
154
148
  config,
149
+ useAbsoluteBalance: useAbsoluteBalanceAndNonce,
155
150
  })
156
151
 
157
152
  const { createToken, getTokens } = createTokenFactory(
@@ -217,15 +212,6 @@ export const createAssetFactory = ({
217
212
  ...args,
218
213
  })
219
214
  break
220
- case 'clarity-v3':
221
- monitor = new ClarityMonitorV2({
222
- assetClientInterface,
223
- interval: ms(monitorInterval || '5m'),
224
- server,
225
- wsGatewayClient,
226
- ...args,
227
- })
228
- break
229
215
  case 'no-history':
230
216
  monitor = new EthereumNoHistoryMonitor({
231
217
  assetClientInterface,
@@ -266,6 +252,7 @@ export const createAssetFactory = ({
266
252
  const sendTx = txSendFactory({
267
253
  assetClientInterface,
268
254
  createUnsignedTx,
255
+ useAbsoluteBalanceAndNonce,
269
256
  })
270
257
 
271
258
  const estimateL1DataFee = l1GasOracleAddress
@@ -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 } = this.#decodeCursor(cursor)
82
+ let { blockNumber } = decodeCursor(cursor)
49
83
  blockNumber = blockNumber.toString()
50
84
 
51
85
  let transactions = []
52
86
 
53
87
  while (true) {
54
- const url = new URL(`${this.baseApiPath}/addresses/${address}/transactions`)
55
- url.searchParams.set('cursor', blockNumber)
56
- url.searchParams.set('withInput', 'true')
57
-
58
- const { transactions: txs, cursor: nextBlockNumber } = await fetchJsonRetry(url)
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 = {
@@ -130,4 +151,14 @@ export default class ClarityServerV2 extends ClarityServer {
130
151
  body: request,
131
152
  })
132
153
  }
154
+
155
+ async getTransactionCount(...params) {
156
+ // nonce is called during tx send, use it in rest api
157
+ const request = this.getTransactionCountRequest(...params)
158
+ return this.sendHttpRequest({
159
+ path: '/rpc',
160
+ method: 'POST',
161
+ body: request,
162
+ })
163
+ }
133
164
  }
@@ -13,7 +13,7 @@ import ApiCoinNodesServer from './api-coin-nodes.js'
13
13
  import ClarityServer from './clarity.js'
14
14
  import ClarityServerV2 from './clarity-v2.js'
15
15
 
16
- export const ValidMonitorTypes = ['no-history', 'clarity', 'clarity-v2', 'clarity-v3', 'magnifier']
16
+ export const ValidMonitorTypes = ['no-history', 'clarity', 'clarity-v2', 'magnifier']
17
17
 
18
18
  export function createEvmServer({ assetName, serverUrl, monitorType }) {
19
19
  assert(assetName, 'assetName is required')
@@ -25,7 +25,6 @@ export function createEvmServer({ assetName, serverUrl, monitorType }) {
25
25
  case 'clarity':
26
26
  return new ClarityServer({ baseAssetName: assetName, uri: serverUrl })
27
27
  case 'clarity-v2':
28
- case 'clarity-v3':
29
28
  return new ClarityServerV2({ baseAssetName: assetName, uri: serverUrl })
30
29
  case 'magnifier':
31
30
  return create(serverUrl, assetName)
@@ -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 EXTRA_PERCENTAGE = 29
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 = ({ estimatedGasLimit, extraPercentage = EXTRA_PERCENTAGE }) =>
16
- Number((BigInt(estimatedGasLimit) * BigInt(100 + extraPercentage)) / BigInt(100))
17
-
18
- const hardcodedGasLimits = new Map([
19
- ['amp', 151_000],
20
- ['tetherusd', 70_000],
21
- ['usdcoin', 70_000],
22
- ['snx', 220_000],
23
- ['geminidollar', 75_000],
24
- ['aave', 250_000],
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
- return scaleGasLimitEstimate({
56
- estimatedGasLimit: BigInt(estimatedGas),
57
- extraPercentage,
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.toBaseString()))
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
- fromAddress,
70
- toAddress,
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
- if (!amount) amount = asset.currency.ZERO
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
- if (isEthereumLikeToken(asset)) {
89
- amount = asset.baseAsset.currency.ZERO
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
- // calling forwarder contracts with a bumped gas limit causes 'Out Of Gas' error on chain
96
- // Since geth v1.9.14 estimateGas will throw if user does not have enough ETH.
97
- // If gasPrice is set to zero, estimateGas will make the expected estimation.
98
- const gasPrice = bufferToHex(currency2buffer(asset.baseAsset.currency.ZERO))
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
- return await estimateGasLimit(
153
+ const estimatedGasLimit = await estimateGasLimit(
102
154
  asset,
103
155
  fromAddress,
104
- toAddress,
105
- amount,
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