@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 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.36.0",
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": "2172464a34a03cb2c2401f80db89d4609cb0528f"
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
+ }
@@ -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, ValidMonitorTypes } from './exodus-eth-server/index.js'
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(serverUrl, 'serverUrl is required')
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
- ...defaultConfig,
113
- ...config,
114
- }
111
+ privacyRpcUrl,
112
+ } = configWithOverrides
115
113
 
116
114
  const asset = assets[base.name]
117
115
 
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
- }
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: (...args) => server.sendRawTransaction(...args),
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 } = 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 = {
@@ -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
 
@@ -18,7 +18,9 @@ export const getAbsoluteBalance = ({ asset, txLog }) => {
18
18
  let balance = asset.currency.ZERO
19
19
  let hasAbsoluteBalance = false
20
20
 
21
- for (const tx of txLog.reverse()) {
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))
@@ -1,53 +1,9 @@
1
1
  import assert from 'minimalistic-assert'
2
2
 
3
- import { isForwarderContractCached } from './eth-like-util.js'
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
- if (!amount) amount = asset.currency.ZERO
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 extraPercentage = await resolveExtraPercentage({
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
- extraPercentage,
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, isExchange, amount }) => {
44
+ ({ asset, feeData, customFee, gasLimit: providedGasLimit, amount }) => {
45
45
  const {
46
46
  feeData: { tipGasPrice, eip1559Enabled },
47
47
  gasPrice,
package/src/index.js CHANGED
@@ -21,8 +21,8 @@ export {
21
21
  } from './eth-like-util.js'
22
22
 
23
23
  export {
24
- EXTRA_PERCENTAGE,
25
24
  DEFAULT_CONTRACT_GAS_LIMIT,
25
+ DEFAULT_GAS_LIMIT_MULTIPLIER,
26
26
  estimateGasLimit,
27
27
  resolveDefaultTxInput,
28
28
  fetchGasLimit,
@@ -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
- estimatedGasLimit + EXTRA_GAS_LIMIT,
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
@@ -48,6 +48,7 @@ const getFeeInfo = async function getFeeInfo({
48
48
  toAddress,
49
49
  amount,
50
50
  txInput,
51
+ feeData,
51
52
  throwOnError: false,
52
53
  }))
53
54
 
@@ -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
- for (const tx of txLog.reverse()) {
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
  }