@exodus/ethereum-api 8.7.1 → 8.9.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 +28 -0
- package/package.json +3 -3
- package/src/create-asset-plugin-factory.js +162 -0
- package/src/create-asset.js +7 -2
- package/src/create-token-factory.js +3 -3
- package/src/error-wrapper.js +89 -0
- package/src/eth-like-util.js +27 -7
- package/src/exodus-eth-server/api-coin-nodes.js +1 -0
- package/src/exodus-eth-server/clarity.js +1 -0
- package/src/gas-estimation.js +1 -0
- package/src/get-balances.js +34 -28
- package/src/index.js +5 -0
- package/src/nft-utils.js +6 -1
- package/src/tx-log/clarity-monitor.js +4 -2
- package/src/tx-log/ethereum-monitor.js +9 -6
- package/src/tx-log/ethereum-no-history-monitor.js +5 -9
- package/src/tx-send/tx-send.js +40 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,34 @@
|
|
|
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.9.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.8.0...@exodus/ethereum-api@8.9.0) (2024-07-03)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* dynamic evm factory ([#2659](https://github.com/ExodusMovement/assets/issues/2659)) ([0baae82](https://github.com/ExodusMovement/assets/commit/0baae82a7f53c8808c9cd2621cf04e9960568cc6))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **ethereum:** false dropped tx check ([#2707](https://github.com/ExodusMovement/assets/issues/2707)) ([d05a8c8](https://github.com/ExodusMovement/assets/commit/d05a8c895563fb13120956c0081db7ebe86195d6))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## [8.8.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.7.1...@exodus/ethereum-api@8.8.0) (2024-07-02)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* 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)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Bug Fixes
|
|
29
|
+
|
|
30
|
+
* ethereum-api default url in servers ([#2689](https://github.com/ExodusMovement/assets/issues/2689)) ([2fc70a4](https://github.com/ExodusMovement/assets/commit/2fc70a49c05f18f6153009b1cebdd9ea522509ac))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
6
34
|
## [8.7.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.7.0...@exodus/ethereum-api@8.7.1) (2024-06-25)
|
|
7
35
|
|
|
8
36
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.9.0",
|
|
4
4
|
"description": "Ethereum Api",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"lint:fix": "yarn lint --fix"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@exodus/asset": "^1.2.0",
|
|
23
24
|
"@exodus/asset-lib": "^4.2.0",
|
|
24
25
|
"@exodus/assets": "^9.1.1",
|
|
25
26
|
"@exodus/basic-utils": "^2.1.0",
|
|
@@ -48,7 +49,6 @@
|
|
|
48
49
|
"ws": "^6.1.0"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
|
-
"@exodus/assets": "^9.1.1",
|
|
52
52
|
"@exodus/assets-testing": "^1.0.0",
|
|
53
53
|
"@exodus/bsc-meta": "^1.2.2",
|
|
54
54
|
"@exodus/ethereumarbone-meta": "^1.2.0",
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"type": "git",
|
|
67
67
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "237c799e9bc8df6574be95056a999896e2503290"
|
|
70
70
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createMetaDef } from '@exodus/asset'
|
|
2
|
+
import { FeeData } from '@exodus/asset-lib'
|
|
3
|
+
import { createAssetFactory } from './create-asset'
|
|
4
|
+
import assert from 'minimalistic-assert'
|
|
5
|
+
import { UnitType } from '@exodus/currency'
|
|
6
|
+
|
|
7
|
+
// Move to ethereum-api for config base evm
|
|
8
|
+
export const createAssetPluginFactory = (config) => {
|
|
9
|
+
assert(config, 'config is required')
|
|
10
|
+
assert(config.chainId, 'chainId is required')
|
|
11
|
+
assert(config.meta, 'config.meta is required')
|
|
12
|
+
assert(config.meta.explorerUrl, 'meta.explorerUrl is required')
|
|
13
|
+
|
|
14
|
+
const chainId = config.chainId
|
|
15
|
+
const { explorerUrl, ...meta } = config.meta
|
|
16
|
+
const name = `evm_${chainId}`
|
|
17
|
+
const ticker = `EVM${chainId}`
|
|
18
|
+
const tokenType = `EVM_${chainId}_TOKEN`
|
|
19
|
+
|
|
20
|
+
const units = {
|
|
21
|
+
wei: 0,
|
|
22
|
+
Kwei: 3,
|
|
23
|
+
Mwei: 6,
|
|
24
|
+
Gwei: 9,
|
|
25
|
+
[ticker]: config.decimals || 18,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const currency = UnitType.create(units)
|
|
29
|
+
|
|
30
|
+
// adds lots of validation in config
|
|
31
|
+
const tokenOverrides = (token) => ({
|
|
32
|
+
...token,
|
|
33
|
+
assetType: tokenType,
|
|
34
|
+
contract: token.addresses,
|
|
35
|
+
gasLimit: meta.gasLimit || 120e3,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const blockExplorer = {
|
|
39
|
+
addressUrl: (address) => `${explorerUrl}/address/${encodeURIComponent(address)}`,
|
|
40
|
+
txUrl: (txId) => `${explorerUrl}/tx/${encodeURIComponent(txId)}`,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const assetType = 'ETHEREUM_LIKE'
|
|
44
|
+
const { asset, assetsList } = createMetaDef({
|
|
45
|
+
assetParams: {
|
|
46
|
+
...meta,
|
|
47
|
+
name,
|
|
48
|
+
ticker,
|
|
49
|
+
units,
|
|
50
|
+
chainId,
|
|
51
|
+
assetType,
|
|
52
|
+
blockExplorer,
|
|
53
|
+
},
|
|
54
|
+
tokensParams: meta.tokens || [],
|
|
55
|
+
tokenOverrides,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const resolveFeeDataConfigDefaults = () => {
|
|
59
|
+
const shared = {
|
|
60
|
+
tipGasPrice: '0 Gwei',
|
|
61
|
+
fuelThreshold: '0 Gwei',
|
|
62
|
+
gasPriceEconomicalRate: 0.8,
|
|
63
|
+
gasPriceMinimumRate: 0.6,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (config.eip1559Enabled === true) {
|
|
67
|
+
assert(config.baseFeePerGas, 'baseFeePerGas is required')
|
|
68
|
+
const baseFeePerGas = currency.parse(config.baseFeePerGas)
|
|
69
|
+
const gasPrice = config.gasPrice ? currency.parse(config.gasPrice) : baseFeePerGas.mul(1.5)
|
|
70
|
+
return {
|
|
71
|
+
gasPrice: gasPrice.toBaseString({ unit: true }),
|
|
72
|
+
baseFeePerGas: baseFeePerGas.toBaseString({ unit: true }),
|
|
73
|
+
max: gasPrice.mul(5).toBaseString({ unit: true }),
|
|
74
|
+
min: gasPrice.div(5).toBaseString({ unit: true }),
|
|
75
|
+
eip1559Enabled: config.eip1559Enabled,
|
|
76
|
+
...shared,
|
|
77
|
+
...config.feeData,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (config.eip1559Enabled === false) {
|
|
82
|
+
assert(config.gasPrice, 'gasPrice is required')
|
|
83
|
+
const gasPrice = currency.parse(config.gasPrice)
|
|
84
|
+
return {
|
|
85
|
+
gasPrice: gasPrice.toBaseString({ unit: true }),
|
|
86
|
+
max: gasPrice.mul(5).toBaseString({ unit: true }),
|
|
87
|
+
min: gasPrice.div(5).toBaseString({ unit: true }),
|
|
88
|
+
eip1559Enabled: config.eip1559Enabled,
|
|
89
|
+
...shared,
|
|
90
|
+
...config.feeData,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
gasPrice: '0.02 Gwei',
|
|
96
|
+
baseFeePerGas: '0.02 Gwei',
|
|
97
|
+
eip1559Enabled: true,
|
|
98
|
+
...shared,
|
|
99
|
+
...config.feeData,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const feeData = new FeeData({
|
|
104
|
+
config: resolveFeeDataConfigDefaults(),
|
|
105
|
+
mainKey: 'gasPrice',
|
|
106
|
+
currency: asset.currency,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const createAsset = createAssetFactory({
|
|
110
|
+
assetsList,
|
|
111
|
+
feeData,
|
|
112
|
+
...config.plugin,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
return { name, chainId, createAsset }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// https://docs.metamask.io/wallet/reference/wallet_addethereumchain/
|
|
119
|
+
export const fromAddEthereumChainParameterToFactoryParams = (params) => {
|
|
120
|
+
assert(params.chainId, 'chainId is required')
|
|
121
|
+
assert(params.chainName, 'chainName is required')
|
|
122
|
+
assert(params.rpcUrls?.[0], 'rpcUrls is required')
|
|
123
|
+
assert(params.blockExplorerUrls?.[0], 'blockExplorerUrls is required')
|
|
124
|
+
assert(params.nativeCurrency, 'nativeCurrency is required')
|
|
125
|
+
assert(params.nativeCurrency.symbol, 'nativeCurrency.symbol is required')
|
|
126
|
+
assert(params.nativeCurrency.decimals, 'nativeCurrency.decimals is required')
|
|
127
|
+
|
|
128
|
+
const chainId = parseInt(params.chainId.replace('0x', ''), 16)
|
|
129
|
+
|
|
130
|
+
// obvious coloring:)
|
|
131
|
+
const color =
|
|
132
|
+
params.color ||
|
|
133
|
+
'#' +
|
|
134
|
+
(chainId % 256).toString(16) +
|
|
135
|
+
((chainId + 50) % 256).toString(16) +
|
|
136
|
+
((chainId + 100) % 256).toString(16)
|
|
137
|
+
return {
|
|
138
|
+
chainId,
|
|
139
|
+
meta: {
|
|
140
|
+
explorerUrl: params.blockExplorerUrls[0],
|
|
141
|
+
displayName: params.chainName,
|
|
142
|
+
displayTicker: params.nativeCurrency.name || params.nativeCurrency.symbol,
|
|
143
|
+
primaryColor: color,
|
|
144
|
+
gradientColors: [color, color],
|
|
145
|
+
gradientCoords: {
|
|
146
|
+
x1: '0%',
|
|
147
|
+
y1: '0%',
|
|
148
|
+
x2: '100%',
|
|
149
|
+
y2: '100%',
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
plugin: {
|
|
153
|
+
serverUrl: params.rpcUrls?.[0],
|
|
154
|
+
confirmationsNumber: 5,
|
|
155
|
+
monitorType: 'no-history',
|
|
156
|
+
isTestnet: params.isTestnet,
|
|
157
|
+
},
|
|
158
|
+
baseFeePerGas: params.baseFeePerGas,
|
|
159
|
+
gasPrice: params.gasPrice,
|
|
160
|
+
eip1559Enabled: params.eip1559Enabled,
|
|
161
|
+
}
|
|
162
|
+
}
|
package/src/create-asset.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { connectAssetsList } from '@exodus/assets'
|
|
2
2
|
import { pick } from '@exodus/basic-utils'
|
|
3
3
|
import bip44Constants from '@exodus/bip44-constants/by-ticker'
|
|
4
|
-
import {
|
|
4
|
+
import { getBalancesFactory } from './get-balances'
|
|
5
5
|
import { ClarityMonitor } from './tx-log/clarity-monitor'
|
|
6
6
|
import { EthereumNoHistoryMonitor } from './tx-log/ethereum-no-history-monitor'
|
|
7
7
|
import { EthereumMonitor } from './tx-log/ethereum-monitor'
|
|
@@ -108,7 +108,12 @@ export const createAssetFactory = ({
|
|
|
108
108
|
encodePublic: encodePublicFactory({ chainId, useEip1191ChainIdChecksum }),
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
const
|
|
111
|
+
const getBalances = getBalancesFactory({ monitorType })
|
|
112
|
+
|
|
113
|
+
const { createToken, getTokens } = createTokenFactory(
|
|
114
|
+
{ address, bip44, keys, getBalances },
|
|
115
|
+
assets
|
|
116
|
+
)
|
|
112
117
|
|
|
113
118
|
const addressHasHistory = addressHasHistoryFactory({ server })
|
|
114
119
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { isPolygonClaimUndelegate, isPolygonDelegate, isPolygonUndelegate } from './staking/matic'
|
|
2
2
|
import { createContract } from '@exodus/ethereum-lib'
|
|
3
3
|
|
|
4
|
-
import { getBalances } from './get-balances'
|
|
5
|
-
|
|
6
4
|
import { createStakingApi } from './staking-api'
|
|
5
|
+
import assert from 'minimalistic-assert'
|
|
7
6
|
|
|
8
7
|
const defaultTokenFeatures = {
|
|
9
8
|
isMaxFeeAsset: true,
|
|
@@ -30,8 +29,9 @@ const getPolygonActivityTxs = ({ txs }) =>
|
|
|
30
29
|
const getActivityTxs = ({ txs }) => txs.filter((tx) => !smallbalanceTx(tx))
|
|
31
30
|
|
|
32
31
|
const getCreateBaseToken =
|
|
33
|
-
(props) =>
|
|
32
|
+
({ getBalances, ...props }) =>
|
|
34
33
|
({ name, contract, features, ...tokenDef }) => {
|
|
34
|
+
assert(getBalances, 'getBalances is required')
|
|
35
35
|
const tokenSpecificFeatures = name === 'polygon' ? { staking: {} } : {}
|
|
36
36
|
const tokenSpecificApiFunctions =
|
|
37
37
|
name === 'polygon' ? { staking: createStakingApi({ network: name }) } : {}
|
|
@@ -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
|
+
}
|
package/src/eth-like-util.js
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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)
|
package/src/gas-estimation.js
CHANGED
package/src/get-balances.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { isRpcBalanceAsset } from '@exodus/ethereum-lib'
|
|
2
2
|
|
|
3
3
|
import { get } from 'lodash'
|
|
4
|
+
import assert from 'minimalistic-assert'
|
|
5
|
+
|
|
4
6
|
import {
|
|
5
7
|
getBalanceFromAccountState,
|
|
6
8
|
getBalanceFromTxLog,
|
|
@@ -30,36 +32,40 @@ const getUnstaked = ({ accountState, asset }) => {
|
|
|
30
32
|
* @param accountState the account state when the balance is loaded from RPC
|
|
31
33
|
* @returns {{balance}|null} an object with the balance or null if the balance is unknown
|
|
32
34
|
*/
|
|
33
|
-
export const
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
export const getBalancesFactory = ({ monitorType }) => {
|
|
36
|
+
assert(monitorType, 'monitorType is required')
|
|
37
|
+
return ({ asset, txLog, accountState }) => {
|
|
38
|
+
const unconfirmedReceived = getUnconfirmedReceivedBalance({ asset, txLog })
|
|
39
|
+
const unconfirmedSent = getUnconfirmedSentBalance({ asset, txLog })
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
// balance from txLog / rpc is considered to be total
|
|
42
|
+
const balanceWithoutUnconfirmedSent =
|
|
43
|
+
monitorType === 'no-history' || isRpcBalanceAsset(asset)
|
|
44
|
+
? getBalanceFromAccountState({ asset, accountState }).sub(unconfirmedSent)
|
|
45
|
+
: getBalanceFromTxLog({ txLog, asset })
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
const staked = getStaked({ asset, accountState })
|
|
48
|
+
const unstaking = getUnstaking({ asset, accountState })
|
|
49
|
+
const unstaked = getUnstaked({ asset, accountState })
|
|
50
|
+
const total = balanceWithoutUnconfirmedSent
|
|
51
|
+
const spendable = balanceWithoutUnconfirmedSent
|
|
52
|
+
.sub(unconfirmedReceived)
|
|
53
|
+
.sub(staked)
|
|
54
|
+
.sub(unstaking)
|
|
55
|
+
.sub(unstaked)
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
return {
|
|
58
|
+
// new
|
|
59
|
+
spendable,
|
|
60
|
+
total,
|
|
61
|
+
staked,
|
|
62
|
+
unstaking,
|
|
63
|
+
unstaked,
|
|
64
|
+
unconfirmedReceived,
|
|
65
|
+
unconfirmedSent,
|
|
66
|
+
// legacy
|
|
67
|
+
balance: total,
|
|
68
|
+
spendableBalance: spendable,
|
|
69
|
+
}
|
|
64
70
|
}
|
|
65
71
|
}
|
package/src/index.js
CHANGED
|
@@ -11,5 +11,10 @@ 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'
|
|
17
|
+
export {
|
|
18
|
+
createAssetPluginFactory,
|
|
19
|
+
fromAddEthereumChainParameterToFactoryParams,
|
|
20
|
+
} from './create-asset-plugin-factory'
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
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 }) {
|
|
@@ -120,8 +123,8 @@ export class EthereumMonitor extends BaseMonitor {
|
|
|
120
123
|
// FIXME: Fetching unconfirmed txs from node helps to avoid
|
|
121
124
|
// flagging on-chain confirmed txs as dropped in the wallet.
|
|
122
125
|
// Once the real bug is found, remove this logic
|
|
123
|
-
const
|
|
124
|
-
const isTxConfirmed =
|
|
126
|
+
const rpcTx = await this.server.getTransactionByHash(tx.txId)
|
|
127
|
+
const isTxConfirmed = !!rpcTx?.blockNumber
|
|
125
128
|
this.logger.warn(`tx ${tx.txId} confirmed: ${isTxConfirmed}`)
|
|
126
129
|
await updateTx(tx, assetName, { isTxConfirmed })
|
|
127
130
|
}
|
|
@@ -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
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
const uri = config?.server || this.server.defaultUri
|
|
32
|
+
|
|
33
|
+
if (uri === this.server.uri) {
|
|
37
34
|
return
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
this.server.setURI(config.server)
|
|
37
|
+
this.server.setURI(uri)
|
|
42
38
|
}
|
|
43
39
|
|
|
44
40
|
async getBalances({ tokens, ourWalletAddress }) {
|
package/src/tx-send/tx-send.js
CHANGED
|
@@ -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 (
|
|
137
|
-
throw
|
|
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
|
-
|
|
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
|
|