@exodus/ethereum-api 8.7.0 → 8.8.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,29 @@
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.8.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.7.1...@exodus/ethereum-api@8.8.0) (2024-07-02)
7
+
8
+
9
+ ### Features
10
+
11
+ * Attach some error reasons to create tx ([#2649](https://github.com/ExodusMovement/assets/issues/2649)) ([0f783e2](https://github.com/ExodusMovement/assets/commit/0f783e2a40079ce407b366e0f6411d279c1fb0ce)), closes [#2646](https://github.com/ExodusMovement/assets/issues/2646)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * ethereum-api default url in servers ([#2689](https://github.com/ExodusMovement/assets/issues/2689)) ([2fc70a4](https://github.com/ExodusMovement/assets/commit/2fc70a49c05f18f6153009b1cebdd9ea522509ac))
17
+
18
+
19
+
20
+ ## [8.7.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.7.0...@exodus/ethereum-api@8.7.1) (2024-06-25)
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * get fee async txInput ([#2665](https://github.com/ExodusMovement/assets/issues/2665)) ([20f9358](https://github.com/ExodusMovement/assets/commit/20f93587ade7291aff6a44872cbaead93b80d1f3))
26
+
27
+
28
+
6
29
  ## [8.7.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.6.0...@exodus/ethereum-api@8.7.0) (2024-06-21)
7
30
 
8
31
 
@@ -45,6 +68,15 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
45
68
 
46
69
 
47
70
 
71
+ ## 8.4.1-exodus.0
72
+
73
+
74
+ ### Bug Fixes
75
+
76
+ * send fromAddress when approving tokens ([#2600](https://github.com/ExodusMovement/assets/pull/2600) )
77
+
78
+
79
+
48
80
  ## [8.4.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.4.0...@exodus/ethereum-api@8.4.1) (2024-06-13)
49
81
 
50
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "8.7.0",
3
+ "version": "8.8.0",
4
4
  "description": "Ethereum Api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -66,5 +66,5 @@
66
66
  "type": "git",
67
67
  "url": "git+https://github.com/ExodusMovement/assets.git"
68
68
  },
69
- "gitHead": "685abde3787a364cdd00a675e95fe4dfa9053d96"
69
+ "gitHead": "2429beb0c513a4e0da8de7e4c070f0774e5f62c4"
70
70
  }
@@ -0,0 +1,89 @@
1
+ export const reasons = {
2
+ fetchGasLimitFailed: 'Fetch gas limit failed',
3
+ nonceFetchFailed: 'Nonce fetch failed',
4
+ balanceFetchFailed: 'Balance fetch failed',
5
+ broadcastTxFailed: 'Broadcast tx failed',
6
+ getTransactionByHashFailed: 'Get transaction by hash failed',
7
+ ethCallErc20Failed: 'Eth call erc20 failed',
8
+ insufficientFunds: 'Insufficient funds',
9
+ bumpTxFailed: 'Bump tx failed',
10
+ }
11
+
12
+ const MAX_HINT_LENGTH = 100
13
+ // TODO: move this to be an generic error for all assets
14
+ export class EthLikeError extends Error {
15
+ #hintStack
16
+
17
+ /**
18
+ * Creates an instance of EthLikeError.
19
+ *
20
+ * @param {string} message - Standard error message.
21
+ * @param {string} tag - Tag associated with the error.
22
+ * @param {string} reason - A constant indicating the generic failure. Must not contain any sensitive information such as private keys, transaction IDs, or wallet addresses.
23
+ * @param {string} hint - A hint to help the user understand the error. Must not contain any sensitive information such as private keys, transaction IDs, or wallet addresses.
24
+ */
25
+ constructor({ message, reason, hint }) {
26
+ super(message)
27
+ this.name = 'EthLikeError'
28
+ this.#hintStack = [hint] // NOTE: we can add more hints to the stack
29
+ this.reason = reason
30
+ this.hint = this.#extractHint(hint)
31
+ }
32
+
33
+ addHint = (hint) => {
34
+ const filteredHint = this.#extractHint(hint)
35
+ if (!filteredHint) {
36
+ return this
37
+ }
38
+
39
+ this.#hintStack.push(hint)
40
+ this.hint = `${this.#hintStack.join(':')}`
41
+ return this
42
+ }
43
+
44
+ // NOTE: a class method for now, if we move this to be a generic error for all assets, each asset will have its own implementation
45
+ #extractHint = (hint) => {
46
+ if (!hint) return ''
47
+
48
+ // Define regex patterns for sensitive information
49
+ const sensitivePatterns = [
50
+ /(?:0x)?[\dA-Fa-f]{64}/g, // Pattern for transaction hashes (hex string of 64 characters, optional 0x prefix)
51
+ /(?:0x)?[\dA-Fa-f]{40}/g, // Pattern for wallet addresses (hex string of 40 characters, optional 0x prefix)
52
+ ]
53
+
54
+ let filteredHint = hint
55
+
56
+ // Check for sensitive information
57
+ const containsSensitiveInfo = sensitivePatterns.some((pattern) => pattern.test(filteredHint))
58
+
59
+ if (containsSensitiveInfo) {
60
+ console.warn('Hint contains sensitive information and will be ignored.')
61
+ return ''
62
+ }
63
+
64
+ // Limit length to 100 characters
65
+ if (filteredHint.length > MAX_HINT_LENGTH) {
66
+ filteredHint = filteredHint.slice(0, MAX_HINT_LENGTH - 3) + '...'
67
+ }
68
+
69
+ return filteredHint
70
+ }
71
+ }
72
+
73
+ export const withErrorReason = async (promise, errorReason, hint) => {
74
+ try {
75
+ return await promise
76
+ } catch (err) {
77
+ if (err instanceof EthLikeError) {
78
+ // ignore errorReason and add hint to stack
79
+ err.addHint(hint)
80
+ throw err
81
+ }
82
+
83
+ throw new EthLikeError({
84
+ message: err.message,
85
+ reason: errorReason,
86
+ hint,
87
+ })
88
+ }
89
+ }
@@ -4,6 +4,7 @@ import { memoizeLruCache } from '@exodus/asset-lib'
4
4
  import SolidityContract from '@exodus/solidity-contract'
5
5
  import { getServerByName, getServer } from './exodus-eth-server'
6
6
  import { fromHexToString } from './number-utils'
7
+ import * as ErrorWrapper from './error-wrapper'
7
8
 
8
9
  export async function isContract(baseAssetName, address) {
9
10
  return getServerByName(baseAssetName).isContract(address)
@@ -35,17 +36,27 @@ export const isForwarderContractCached = memoizeLruCache(
35
36
 
36
37
  export async function getNonce({ asset, address, tag = 'latest' }) {
37
38
  const server = getServer(asset)
38
- const nonce = await server.getTransactionCount(address, tag)
39
+ const nonce = await ErrorWrapper.withErrorReason(
40
+ server.getTransactionCount(address, tag),
41
+ ErrorWrapper.reasons.nonceFetchFailed,
42
+ 'getNonce'
43
+ )
44
+
39
45
  return parseInt(nonce, 16)
40
46
  }
41
47
 
42
48
  export async function estimateGas({ asset, ...args }) {
43
- return getServer(asset).estimateGas(args)
49
+ return ErrorWrapper.withErrorReason(
50
+ getServer(asset).estimateGas(args),
51
+ ErrorWrapper.reasons.fetchGasLimitFailed,
52
+ 'estimateGas'
53
+ )
44
54
  }
45
55
 
46
56
  // Only base assets, not tokens
47
57
  export async function getBalance({ asset, address }) {
48
58
  if (!isEthereumLikeAsset(asset)) throw new Error(`unsupported asset ${asset.name}`)
59
+
49
60
  const server = getServer(asset)
50
61
  const balances = await server.getBalance(address)
51
62
  return balances?.confirmed?.value || '0'
@@ -54,6 +65,7 @@ export async function getBalance({ asset, address }) {
54
65
  // Only base assets, not tokens
55
66
  export async function getBalanceProxied({ asset, address, tag = 'latest' }) {
56
67
  if (!isEthereumLikeAsset(asset)) throw new Error(`unsupported asset ${asset.name}`)
68
+
57
69
  const result = await getServer(asset).getBalanceProxied(address)
58
70
  return fromHexToString(result)
59
71
  }
@@ -61,6 +73,7 @@ export async function getBalanceProxied({ asset, address, tag = 'latest' }) {
61
73
  // Only ETH-like assets with token support
62
74
  export async function getTokenBalance({ asset, address }) {
63
75
  if (!isEthereumLikeToken(asset)) throw new Error(`unsupported ETH-like token ${asset.name}`)
76
+
64
77
  const server = getServer(asset)
65
78
  const balances = await server.getBalance(address)
66
79
  const contractAddress = asset.contract.address.toLowerCase()
@@ -69,6 +82,7 @@ export async function getTokenBalance({ asset, address }) {
69
82
 
70
83
  export async function getTokenBalanceFromNode({ asset, address }) {
71
84
  if (!isEthereumLikeToken(asset)) throw new Error(`unsupported ETH-like token ${asset.name}`)
85
+
72
86
  const server = getServer(asset)
73
87
  const contractAddress = asset.contract.address.toLowerCase()
74
88
  const balances = await server.balanceOf(address, contractAddress)
@@ -83,7 +97,7 @@ export function sendRawTransaction(asset) {
83
97
  export async function transactionExists({ asset, txId }) {
84
98
  const server = getServer(asset)
85
99
  txId = normalizeTxId(txId)
86
- const txResult = await server.getTransactionByHash(txId)
100
+ const txResult = server.getTransactionByHash(txId)
87
101
  return txResult && txResult.hash === txId
88
102
  }
89
103
 
@@ -122,12 +136,18 @@ export const getERC20Params = async ({ asset, address, paramNames = DEFAULT_PARA
122
136
  callResponse = await server.ethCall({ to: address, data: ERC20[method].methodId })
123
137
  } catch (err) {
124
138
  if (err.message === 'execution reverted') {
125
- throw new Error(
126
- `Can't find parameters for contract with address ${address}. Are you sure it is a valid ERC20 contract?`
127
- )
139
+ throw new ErrorWrapper.EthLikeError({
140
+ message: `Can't find parameters for contract with address ${address}. Are you sure it is a valid ERC20 contract?`,
141
+ reason: ErrorWrapper.reasons.ethCallErc20Failed,
142
+ hint: 'ethCall:executionReverted',
143
+ })
128
144
  }
129
145
 
130
- throw new Error(err.message)
146
+ throw new ErrorWrapper.EthLikeError({
147
+ message: err.message,
148
+ reason: ErrorWrapper.reasons.ethCallErc20Failed,
149
+ hint: 'ethCall',
150
+ })
131
151
  }
132
152
 
133
153
  if (method === 'decimals') return parseInt(callResponse)
@@ -9,6 +9,7 @@ export default class ApiCoinNodesServer extends EventEmitter {
9
9
  constructor({ uri }) {
10
10
  super()
11
11
  this.uri = uri
12
+ this.defaultUri = uri
12
13
  this.id = 0
13
14
  }
14
15
 
@@ -9,6 +9,7 @@ export default class ClarityServer extends EventEmitter {
9
9
  super()
10
10
  this.baseAssetName = baseAssetName
11
11
  this.uri = uri
12
+ this.defaultUri = uri
12
13
  this.baseNamespace = `/v1/${this.baseAssetName}`
13
14
  this.sockets = Object.create(null)
14
15
  this.id = 0
@@ -38,6 +38,14 @@ export async function estimateGasLimit(
38
38
  .toNumber()
39
39
  }
40
40
 
41
+ export function resolveDefaultTxInput({ asset, toAddress, amount }) {
42
+ return isEthereumLikeToken(asset)
43
+ ? ethUtil.bufferToHex(
44
+ asset.contract.transfer.build(toAddress.toLowerCase(), amount.toBaseString())
45
+ )
46
+ : '0x'
47
+ }
48
+
41
49
  export async function fetchGasLimit({
42
50
  asset,
43
51
  fromAddress,
@@ -53,22 +61,14 @@ export async function fetchGasLimit({
53
61
 
54
62
  if (!amount) amount = asset.currency.ZERO
55
63
 
56
- const isToken = isEthereumLikeToken(asset)
57
-
58
- const txInput =
59
- providedTxInput ||
60
- (isToken
61
- ? ethUtil.bufferToHex(
62
- asset.contract.transfer.build(toAddress.toLowerCase(), amount.toBaseString())
63
- )
64
- : '0x')
64
+ const txInput = providedTxInput || resolveDefaultTxInput({ asset, toAddress, amount })
65
65
 
66
66
  const defaultGasLimit = () =>
67
67
  asset.gasLimit + GAS_PER_NON_ZERO_BYTE * ethUtil.toBuffer(txInput).length
68
68
 
69
69
  const isContract = await isContractAddressCached({ asset, address: toAddress })
70
70
 
71
- if (isToken) {
71
+ if (isEthereumLikeToken(asset)) {
72
72
  amount = asset.baseAsset.currency.ZERO
73
73
  toAddress = asset.contract.address
74
74
  } else if (
@@ -102,6 +102,7 @@ export async function fetchGasLimit({
102
102
  )
103
103
  } catch (err) {
104
104
  if (throwOnError) throw err
105
+
105
106
  console.error('fetchGasLimit error', err)
106
107
 
107
108
  // fallback value for contract case
@@ -1,5 +1,5 @@
1
1
  import assert from 'minimalistic-assert'
2
- import { fetchGasLimit } from './gas-estimation'
2
+ import { fetchGasLimit, resolveDefaultTxInput } from './gas-estimation'
3
3
  import { isForwarderContractCached } from './eth-like-util'
4
4
  import { getFeeFactory } from './get-fee'
5
5
  import { getNftArguments } from './nft-utils'
@@ -52,13 +52,14 @@ const getFeeAsyncFactory = ({
52
52
  return getNftArguments({ asset, nft, fromAddress, toAddress })
53
53
  }
54
54
 
55
+ if (!amount) amount = asset.currency.ZERO
55
56
  const extraPercentage = await resolveExtraPercentage({ asset, toAddress })
56
- const txInput = txInputPram ?? '0x'
57
+ const txInput = txInputPram || resolveDefaultTxInput({ asset, toAddress, amount })
57
58
  const gasLimit = await fetchGasLimit({
58
59
  asset,
59
60
  fromAddress,
60
61
  toAddress,
61
- txInput, // default value is '0x'
62
+ txInput,
62
63
  amount,
63
64
  bip70,
64
65
  extraPercentage,
@@ -104,7 +105,6 @@ const getFeeAsyncFactory = ({
104
105
  fee: fee.add(l1DataFee),
105
106
  optimismL1DataFee,
106
107
  gasLimit,
107
- txInput,
108
108
  ...rest,
109
109
  }
110
110
  }
package/src/index.js CHANGED
@@ -11,5 +11,6 @@ export * from './simulate-tx'
11
11
  export * from './allowance'
12
12
  export * from './optimism-gas'
13
13
  export * from './number-utils'
14
+ export { reasons as errorReasons, withErrorReason, EthLikeError } from './error-wrapper'
14
15
  export { txSendFactory, getFeeInfo } from './tx-send'
15
16
  export { createAssetFactory } from './create-asset'
package/src/nft-utils.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import SolidityContract from '@exodus/solidity-contract'
2
2
  import { fetchGasLimit } from './gas-estimation'
3
3
  import assert from 'minimalistic-assert'
4
+ import * as ErrorWrapper from './error-wrapper'
4
5
 
5
6
  export const getNftArguments = async ({ asset, nft, fromAddress, toAddress }) => {
6
7
  assert(asset, 'asset is required')
@@ -43,7 +44,11 @@ export const getNftArguments = async ({ asset, nft, fromAddress, toAddress }) =>
43
44
  }
44
45
  })
45
46
  ).catch((e) => {
46
- throw new Error(errors.join('\n'))
47
+ throw new ErrorWrapper.EthLikeError({
48
+ message: errors.join('\n'),
49
+ reason: ErrorWrapper.reasons.fetchGasLimitFailed,
50
+ hint: 'getNftArguments',
51
+ })
47
52
  })
48
53
  return {
49
54
  contractAddress,
@@ -26,11 +26,13 @@ export class ClarityMonitor extends BaseMonitor {
26
26
  }
27
27
 
28
28
  setServer(config) {
29
- if (!config?.server || config.server === this.server.uri) {
29
+ const uri = config?.server || this.server.defaultUri
30
+
31
+ if (uri === this.server.uri) {
30
32
  return
31
33
  }
32
34
 
33
- this.server.setURI(config.server)
35
+ this.server.setURI(uri)
34
36
  this.subscribeWalletAddresses()
35
37
  if (this.config.GAS_PRICE_FROM_WEBSOCKET) {
36
38
  this.server.connectFee()
@@ -1,4 +1,3 @@
1
- import { getServer } from '../exodus-eth-server'
2
1
  import { isRpcBalanceAsset, getAssetAddresses } from '@exodus/ethereum-lib'
3
2
 
4
3
  import {
@@ -28,7 +27,7 @@ import { fromHexToString } from '../number-utils'
28
27
  export class EthereumMonitor extends BaseMonitor {
29
28
  constructor({ server, config, ...args }) {
30
29
  super(args)
31
- this.server = server || getServer(this.asset)
30
+ this.server = server
32
31
  this.config = { GAS_PRICE_FROM_WEBSOCKET: true, ...config }
33
32
  this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
34
33
  this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
@@ -41,9 +40,13 @@ export class EthereumMonitor extends BaseMonitor {
41
40
  }
42
41
 
43
42
  setServer(config = {}) {
44
- if (config.serverv1 && this.server.getURL() !== config.serverv1) {
45
- this.server.setURL(config.serverv1)
43
+ const uri = config?.serverv1 || this.server.defaultUri
44
+
45
+ if (uri === this.server.getURL()) {
46
+ return
46
47
  }
48
+
49
+ this.server.setURL(uri)
47
50
  }
48
51
 
49
52
  async deriveData({ assetSource, tokens }) {
@@ -1,6 +1,3 @@
1
- import { getServer } from '../exodus-eth-server'
2
- // eslint-disable-next-line import/no-deprecated
3
- import { DEFAULT_SERVER_URLS } from '@exodus/ethereum-lib'
4
1
  import { Tx } from '@exodus/models'
5
2
 
6
3
  import {
@@ -22,7 +19,7 @@ import { SynchronizedTime } from '@exodus/basic-utils'
22
19
  export class EthereumNoHistoryMonitor extends BaseMonitor {
23
20
  constructor({ server, config, ...args }) {
24
21
  super(args)
25
- this.server = server || getServer(this.asset)
22
+ this.server = server
26
23
  this.config = { ...config }
27
24
  this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
28
25
  this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
@@ -31,14 +28,13 @@ export class EthereumNoHistoryMonitor extends BaseMonitor {
31
28
  }
32
29
 
33
30
  setServer(config) {
34
- if (!config?.server) {
35
- // eslint-disable-next-line import/no-deprecated
36
- this.server.setURI(DEFAULT_SERVER_URLS[this.asset.name])
31
+ const uri = config?.server || this.server.defaultUri
32
+
33
+ if (uri === this.server.uri) {
37
34
  return
38
35
  }
39
36
 
40
- if (config.server === this.server.uri) return
41
- this.server.setURI(config.server)
37
+ this.server.setURI(uri)
42
38
  }
43
39
 
44
40
  async getBalances({ tokens, ourWalletAddress }) {
@@ -5,6 +5,7 @@ import { calculateBumpedGasPrice, isEthereumLikeToken, normalizeTxId } from '@ex
5
5
  import assert from 'minimalistic-assert'
6
6
  import getFeeInfo from './get-fee-info'
7
7
  import { getNftArguments } from '../nft-utils'
8
+ import * as ErrorWrapper from '../error-wrapper'
8
9
 
9
10
  const txSendFactory = ({ assetClientInterface, createUnsignedTx }) => {
10
11
  assert(assetClientInterface, 'assetClientInterface is required')
@@ -127,20 +128,56 @@ const txSendFactory = ({ assetClientInterface, createUnsignedTx }) => {
127
128
  await baseAsset.api.broadcastTx(rawTx.toString('hex'))
128
129
  } catch (err) {
129
130
  const nonceTooLowErr = err.message.match(/nonce (is |)too low/i)
131
+ const insufficientFundsErr = err.message.match(/insufficient funds/i)
130
132
  const txAlreadyExists = nonceTooLowErr
131
133
  ? await transactionExists({ asset, txId })
132
134
  : err.message.match(/already known/i)
133
135
 
134
136
  if (txAlreadyExists) {
135
137
  console.info('tx already broadcast') // inject logger factory from platform
136
- } else if (bumpTxId || !nonceTooLowErr) {
137
- throw err
138
+ } else if (insufficientFundsErr) {
139
+ throw new ErrorWrapper.EthLikeError({
140
+ message: err.message,
141
+ reason: ErrorWrapper.reasons.insufficientFunds,
142
+ hint: 'broadcastTx',
143
+ })
144
+ } else if (bumpTxId) {
145
+ throw new ErrorWrapper.EthLikeError({
146
+ message: err.message,
147
+ reason: ErrorWrapper.reasons.bumpTxFailed,
148
+ hint: 'broadcastTx',
149
+ })
150
+ } else if (!nonceTooLowErr) {
151
+ throw new ErrorWrapper.EthLikeError({
152
+ message: err.message,
153
+ reason: ErrorWrapper.reasons.broadcastTxFailed,
154
+ hint: 'otherErr:broadcastTx',
155
+ })
138
156
  } else if (nonceTooLowErr) {
139
157
  console.info('trying to send again...') // inject logger factory from platform
140
158
  // let's try to fix the nonce issue
159
+
141
160
  nonce = await getNonce({ asset: baseAsset, address: fromAddress })
142
161
  ;({ txId, rawTx } = await createTx({ ...createTxParams, nonce }))
143
- await baseAsset.api.broadcastTx(rawTx.toString('hex'))
162
+
163
+ try {
164
+ await baseAsset.api.broadcastTx(rawTx.toString('hex'))
165
+ } catch (err) {
166
+ const insufficientFundsErr = err.message.match(/insufficient funds/i)
167
+ if (insufficientFundsErr) {
168
+ throw new ErrorWrapper.EthLikeError({
169
+ message: err.message,
170
+ reason: ErrorWrapper.reasons.insufficientFunds,
171
+ hint: 'retry:broadcastTx',
172
+ })
173
+ }
174
+
175
+ throw new ErrorWrapper.EthLikeError({
176
+ message: err.message,
177
+ reason: ErrorWrapper.reasons.broadcastTxFailed,
178
+ hint: 'retry:broadcastTx',
179
+ })
180
+ }
144
181
  }
145
182
  }
146
183