@exodus/ethereum-api 4.0.1 → 4.0.2-alpha1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/ethereum-api",
3
- "version": "4.0.1",
3
+ "version": "4.0.2-alpha1",
4
4
  "description": "Ethereum Api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -16,7 +16,7 @@
16
16
  "dependencies": {
17
17
  "@exodus/asset-lib": "^3.7.1",
18
18
  "@exodus/crypto": "^1.0.0-rc.0",
19
- "@exodus/ethereum-lib": "^2.26.0",
19
+ "@exodus/ethereum-lib": "^2.26.1-alpha1",
20
20
  "@exodus/ethereumjs-util": "^7.1.0-exodus.6",
21
21
  "@exodus/fetch": "^1.2.1",
22
22
  "@exodus/simple-retry": "^0.0.6",
@@ -35,6 +35,5 @@
35
35
  },
36
36
  "devDependencies": {
37
37
  "@exodus/models": "^8.10.4"
38
- },
39
- "gitHead": "729674ae988309eccf03feefbf891d8b43947577"
38
+ }
40
39
  }
@@ -0,0 +1,191 @@
1
+ import { bufferToHex } from '@exodus/ethereumjs-util'
2
+ import SolidityContract from '@exodus/solidity-contract'
3
+ import EventEmitter from 'events'
4
+
5
+ export default class ApiCoinNodesServer extends EventEmitter {
6
+ constructor({ uri }) {
7
+ super()
8
+ this.uri = uri
9
+ this.id = 0
10
+ }
11
+
12
+ setURI(uri) {
13
+ this.uri = uri
14
+ }
15
+
16
+ async request(body) {
17
+ const options = {
18
+ method: 'POST',
19
+ headers: { 'Content-Type': 'application/json' },
20
+ body: JSON.stringify(body),
21
+ }
22
+
23
+ const response = await fetch(this.uri, options)
24
+ const data = await response.json()
25
+
26
+ return data
27
+ }
28
+
29
+ async sendBatchRequest(batch) {
30
+ const responses = await this.request(batch)
31
+ const isValid = responses.every((response) => {
32
+ return !isNaN(response?.id) && response?.result
33
+ })
34
+ if (responses.length !== batch.length || !isValid) {
35
+ throw new Error('Bad rpc batch response')
36
+ }
37
+ const keyed = responses.reduce((acc, response) => {
38
+ return { ...acc, [`${response.id}`]: response.result }
39
+ }, {})
40
+ return batch.map((request) => keyed[`${request.id}`])
41
+ }
42
+
43
+ async sendRequest(request) {
44
+ const response = await this.request(request)
45
+
46
+ const result = response?.result
47
+ const error = response?.error
48
+ if (error || !result) {
49
+ const message = error?.message || error?.code || 'no result'
50
+ throw new Error(`Bad rpc response: ${message}`)
51
+ }
52
+ return result
53
+ }
54
+
55
+ async isContract(address) {
56
+ const code = await this.getCode(address)
57
+ return code.length > 2
58
+ }
59
+
60
+ buildRequest({ method, params = [] }) {
61
+ return { jsonrpc: '2.0', id: this.id++, method, params }
62
+ }
63
+
64
+ balanceOfRequest(address, tokenAddress, tag = 'latest') {
65
+ const contract = SolidityContract.simpleErc20(tokenAddress)
66
+ const callData = contract.balanceOf.build(address)
67
+ const data = {
68
+ data: bufferToHex(callData),
69
+ to: tokenAddress,
70
+ }
71
+ return this.ethCallRequest(data, tag)
72
+ }
73
+
74
+ getBalanceRequest(address, tag = 'latest') {
75
+ return this.buildRequest({ method: 'eth_getBalance', params: [address, tag] })
76
+ }
77
+
78
+ gasPriceRequest() {
79
+ return this.buildRequest({ method: 'eth_gasPrice' })
80
+ }
81
+
82
+ estimateGasRequest(data, tag = 'latest') {
83
+ return this.buildRequest({ method: 'eth_estimateGas', params: [data, tag] })
84
+ }
85
+
86
+ sendRawTransactionRequest(data) {
87
+ const hex = data.startsWith('0x') ? data : '0x' + data
88
+ return this.buildRequest({ method: 'eth_sendRawTransaction', params: [hex] })
89
+ }
90
+
91
+ getCodeRequest(address, tag = 'latest') {
92
+ return this.buildRequest({ method: 'eth_getCode', params: [address, tag] })
93
+ }
94
+
95
+ getTransactionCountRequest(address, tag = 'latest') {
96
+ return this.buildRequest({ method: 'eth_getTransactionCount', params: [address, tag] })
97
+ }
98
+
99
+ getTransactionByHashRequest(hash) {
100
+ return this.buildRequest({ method: 'eth_getTransactionByHash', params: [hash] })
101
+ }
102
+
103
+ getTransactionReceiptRequest(txhash) {
104
+ return this.buildRequest({ method: 'eth_getTransactionReceipt', params: [txhash] })
105
+ }
106
+
107
+ ethCallRequest(data, tag) {
108
+ return this.buildRequest({ method: 'eth_call', params: [data, tag] })
109
+ }
110
+
111
+ blockNumberRequest() {
112
+ return this.buildRequest({ method: 'eth_blockNumber' })
113
+ }
114
+
115
+ getBlockByNumberRequest(numberHex, isFullTxs = false) {
116
+ return this.buildRequest({ method: 'eth_getBlockByNumber', params: [numberHex, isFullTxs] })
117
+ }
118
+
119
+ simulateRawTransactionRequest(rawTx, applyPending = true) {
120
+ const replaced = rawTx.replace('0x', '')
121
+ return this.buildRequest({
122
+ method: 'debug_simulateRawTransaction',
123
+ params: [replaced, applyPending],
124
+ })
125
+ }
126
+
127
+ async balanceOf(...params) {
128
+ const request = this.balanceOfRequest(...params)
129
+ return this.sendRequest(request)
130
+ }
131
+
132
+ async getBalance(...params) {
133
+ const request = this.getBalanceRequest(...params)
134
+ return this.sendRequest(request)
135
+ }
136
+
137
+ async gasPrice(...params) {
138
+ const request = this.gasPriceRequest(...params)
139
+ return this.sendRequest(request)
140
+ }
141
+
142
+ async estimateGas(...params) {
143
+ const request = this.estimateGasRequest(...params)
144
+ return this.sendRequest(request)
145
+ }
146
+
147
+ async sendRawTransaction(...params) {
148
+ const request = this.sendRawTransactionRequest(...params)
149
+ return this.sendRequest(request)
150
+ }
151
+
152
+ async getCode(...params) {
153
+ const request = this.getCodeRequest(...params)
154
+ return this.sendRequest(request)
155
+ }
156
+
157
+ async getTransactionCount(...params) {
158
+ const request = this.getTransactionCountRequest(...params)
159
+ return this.sendRequest(request)
160
+ }
161
+
162
+ async getTransactionByHash(...params) {
163
+ const request = this.getTransactionByHashRequest(...params)
164
+ return this.sendRequest(request)
165
+ }
166
+
167
+ async getTransactionReceipt(...params) {
168
+ const request = this.getTransactionReceiptRequest(...params)
169
+ return this.sendRequest(request)
170
+ }
171
+
172
+ async ethCall(...params) {
173
+ const request = this.ethCallRequest(...params)
174
+ return this.sendRequest(request)
175
+ }
176
+
177
+ async blockNumber(...params) {
178
+ const request = this.blockNumberRequest(...params)
179
+ return this.sendRequest(request)
180
+ }
181
+
182
+ async getBlockByNumber(...params) {
183
+ const request = this.getBlockByNumberRequest(...params)
184
+ return this.sendRequest(request)
185
+ }
186
+
187
+ async simulateRawTransaction(...params) {
188
+ const request = this.simulateRawTransactionRequest(...params)
189
+ return this.sendRequest(request)
190
+ }
191
+ }
@@ -1,15 +1,28 @@
1
- import { DEFAULT_SERVER_URLS, ETHEREUM_LIKE_ASSETS } from '@exodus/ethereum-lib'
1
+ import {
2
+ DEFAULT_SERVER_URLS,
3
+ ETHEREUM_LIKE_ASSETS,
4
+ ETHEREUM_LIKE_MONITOR_TYPES,
5
+ } from '@exodus/ethereum-lib'
2
6
 
3
7
  import { create } from './api'
4
8
  import ClarityServer from './clarity'
9
+ import ApiCoinNodesServer from './api-coin-nodes'
10
+
11
+ function getAssetServer(assetName) {
12
+ switch (ETHEREUM_LIKE_MONITOR_TYPES[assetName]) {
13
+ case 'no-history':
14
+ return new ApiCoinNodesServer({ uri: DEFAULT_SERVER_URLS[assetName] })
15
+ case 'clarity':
16
+ return new ClarityServer({ baseAssetName: assetName, uri: DEFAULT_SERVER_URLS[assetName] })
17
+ case 'magnifier':
18
+ return create(DEFAULT_SERVER_URLS[assetName], assetName)
19
+ default:
20
+ throw new Error(`No server API found for ${assetName}`)
21
+ }
22
+ }
5
23
 
6
24
  const serverMap = Object.fromEntries(
7
- ETHEREUM_LIKE_ASSETS.map((assetName) => [
8
- assetName,
9
- DEFAULT_SERVER_URLS[assetName].includes('clarity')
10
- ? new ClarityServer({ baseAssetName: assetName, uri: DEFAULT_SERVER_URLS[assetName] })
11
- : create(DEFAULT_SERVER_URLS[assetName], assetName),
12
- ])
25
+ ETHEREUM_LIKE_ASSETS.map((assetName) => [assetName, getAssetServer(assetName)])
13
26
  )
14
27
 
15
28
  // allow self-signed certs
@@ -8,3 +8,4 @@ export * from './fantom'
8
8
  export * from './harmony'
9
9
  export * from './ethereumarbnova'
10
10
  export * from './ethereumarbone'
11
+ export * from './rootstock'
@@ -0,0 +1,14 @@
1
+ import { EthereumLikeFeeMonitor } from '@exodus/ethereum-lib'
2
+ import { getServerByName } from '../exodus-eth-server'
3
+
4
+ const assetName = 'rootstock'
5
+
6
+ export class RootstockFeeMonitor extends EthereumLikeFeeMonitor {
7
+ constructor({ updateFee }) {
8
+ super({
9
+ updateFee,
10
+ assetName,
11
+ getGasPrice: getServerByName(assetName).gasPrice,
12
+ })
13
+ }
14
+ }
@@ -0,0 +1,197 @@
1
+ import BN from 'bn.js'
2
+ import { getServer } from '@exodus/ethereum-api'
3
+ import { Tx } from '@exodus/models'
4
+
5
+ import { getDeriveDataNeededForTick, getDeriveTransactionsToCheck } from './monitor-utils'
6
+
7
+ import { isEmpty, unionBy } from 'lodash'
8
+
9
+ import { BaseMonitor } from '@exodus/asset-lib'
10
+ import { getAssetAddresses } from '@exodus/ethereum-lib'
11
+
12
+ import { UNCONFIRMED_TX_LIMIT } from './monitor-utils/get-derive-transactions-to-check'
13
+
14
+ // The base ethereum monitor no history class handles listening for assets with no history
15
+
16
+ export class EthereumNoHistoryMonitor extends BaseMonitor {
17
+ constructor({ server, config, ...args }) {
18
+ super(args)
19
+ this.server = server || getServer(this.asset)
20
+ this.config = { ...config }
21
+ this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
22
+ this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
23
+ getTxLog: (...args) => this.aci.getTxLog(...args),
24
+ })
25
+ }
26
+
27
+ setServer(config) {
28
+ if (config.server === this.server?.uri) {
29
+ return
30
+ }
31
+ this.server.setURI(config.server)
32
+ }
33
+
34
+ async getBalances({ tokens, ourWalletAddress }) {
35
+ const batch = {}
36
+ const request = this.server.getBalanceRequest(ourWalletAddress)
37
+ batch[this.asset.name] = request
38
+ for (const token of tokens) {
39
+ const request = this.server.balanceOfRequest(ourWalletAddress, token.contract.address)
40
+ batch[token.name] = request
41
+ }
42
+ const pairs = Object.entries(batch)
43
+ if (!pairs.length) {
44
+ return {}
45
+ }
46
+ const requests = pairs.map((pair) => pair[1])
47
+ const responses = await this.server.sendBatchRequest(requests)
48
+ const entries = pairs.map((pair, idx) => {
49
+ const balanceHex = responses[idx]
50
+ const name = pair[0]
51
+ const hex = balanceHex.startsWith('0x') ? balanceHex.slice(2) : balanceHex
52
+ const balance = new BN(hex, 'hex').toString()
53
+ return [name, balance]
54
+ })
55
+ return Object.fromEntries(entries)
56
+ }
57
+
58
+ async getNewAccountState({ tokens, ourWalletAddress }) {
59
+ const asset = this.asset
60
+ const newAccountState = {}
61
+ const balances = await this.getBalances({ tokens, ourWalletAddress })
62
+ const balance = balances[asset.name]
63
+ newAccountState.balance = asset.currency.baseUnit(balance)
64
+ const tokenBalancePairs = Object.entries(balances).filter((entry) => entry[0] !== asset.name)
65
+ const entries = tokenBalancePairs
66
+ .map((pair) => {
67
+ const token = tokens.find((token) => token.name === pair[0])
68
+ const value = token.currency.baseUnit(pair[1] || 0)
69
+ return value.isZero ? null : [token.name, value]
70
+ })
71
+ .filter((pair) => pair)
72
+ const tokenBalances = Object.fromEntries(entries)
73
+ if (!isEmpty(tokenBalances)) newAccountState.tokenBalances = tokenBalances
74
+ return newAccountState
75
+ }
76
+
77
+ async deriveData({ assetSource, tokens }) {
78
+ const { assetName, walletAccount } = assetSource
79
+
80
+ const {
81
+ ourWalletAddress,
82
+ currentAccountState,
83
+ minimumConfirmations,
84
+ } = await this.deriveDataNeededForTick({ assetName, walletAccount })
85
+ const transactionsToCheck = await this.deriveTransactionsToCheck({
86
+ assetName,
87
+ walletAccount,
88
+ tokens,
89
+ ourWalletAddress,
90
+ })
91
+
92
+ return {
93
+ ourWalletAddress,
94
+ currentAccountState,
95
+ minimumConfirmations,
96
+ ...transactionsToCheck,
97
+ }
98
+ }
99
+
100
+ async checkPendingTransactions({
101
+ pendingTransactionsGroupedByAddressAndNonce,
102
+ pendingTransactionsToCheck,
103
+ walletAccount,
104
+ }) {
105
+ const txsToUpdate = []
106
+ const txsToRemove = []
107
+ const now = Date.now()
108
+
109
+ const pendingTransactions = unionBy(
110
+ Object.values(pendingTransactionsGroupedByAddressAndNonce),
111
+ Object.values(pendingTransactionsToCheck),
112
+ 'tx.txId'
113
+ )
114
+
115
+ if (isEmpty(pendingTransactions))
116
+ return {
117
+ txsToUpdate,
118
+ txsToRemove,
119
+ }
120
+
121
+ const batch = []
122
+ for (const { tx } of pendingTransactions) {
123
+ const request = this.server.getTransactionByHashRequest(tx.txId)
124
+ batch.push(request)
125
+ }
126
+ const responses = await this.server.sendBatchRequest(batch)
127
+ const txsFromNode = {}
128
+ responses.forEach(
129
+ (response, index) => (txsFromNode[pendingTransactions[index].tx.txId] = response)
130
+ )
131
+
132
+ for (const { tx, assetName } of pendingTransactions) {
133
+ const txFromNode = txsFromNode[tx.txId]
134
+ if (now - tx.date.getTime() > UNCONFIRMED_TX_LIMIT) {
135
+ txsToRemove.push({
136
+ tx,
137
+ assetSource: { asset: assetName, walletAccount },
138
+ })
139
+ } else if (txFromNode.blockHash !== null) {
140
+ txsToUpdate.push({
141
+ tx,
142
+ assetSource: { asset: assetName, walletAccount },
143
+ })
144
+ }
145
+ }
146
+
147
+ return {
148
+ txsToUpdate,
149
+ txsToRemove,
150
+ }
151
+ }
152
+
153
+ async tick({ refresh, walletAccount }) {
154
+ const assets = await this.aci.getAssetsForNetwork({ baseAssetName: this.asset.name })
155
+ const tokens = Object.values(assets).filter((asset) => asset.baseAsset.name !== asset.name)
156
+ const tokensByAddress = tokens.reduce((map, token) => {
157
+ const addresses = getAssetAddresses(token)
158
+ for (const address of addresses) map.set(address.toLowerCase(), token)
159
+ return map
160
+ }, new Map())
161
+
162
+ const assetSource = { assetName: this.asset.name, walletAccount }
163
+
164
+ const {
165
+ ourWalletAddress,
166
+ pendingTransactionsGroupedByAddressAndNonce,
167
+ pendingTransactionsToCheck,
168
+ } = await this.deriveData({
169
+ assetSource,
170
+ tokens,
171
+ })
172
+
173
+ const accountState = await this.getNewAccountState({
174
+ tokens,
175
+ ourWalletAddress,
176
+ })
177
+
178
+ const { txsToUpdate, txsToRemove } = await this.checkPendingTransactions({
179
+ pendingTransactionsGroupedByAddressAndNonce,
180
+ pendingTransactionsToCheck,
181
+ walletAccount,
182
+ })
183
+
184
+ const allAssets = [this.asset, ...tokensByAddress.values()]
185
+ const logItemsByAsset = Object.fromEntries(allAssets.map((asset) => [asset.name, []]))
186
+
187
+ txsToUpdate.forEach((txToUpdate) => {
188
+ const { tx } = txToUpdate
189
+ logItemsByAsset[txToUpdate.assetSource.asset].push(Tx.fromJSON({ ...tx, confirmations: 1 }))
190
+ })
191
+
192
+ await this.updateAccountState({ newData: { ...accountState }, walletAccount })
193
+
194
+ await this.removeFromTxLog(txsToRemove)
195
+ await this.updateTxLogByAsset({ logItemsByAsset, walletAccount, refresh })
196
+ }
197
+ }
@@ -1,2 +1,3 @@
1
1
  export * from './ethereum-monitor'
2
+ export * from './ethereum-no-history-monitor'
2
3
  export { ClarityMonitor } from './clarity-monitor'
@@ -1,7 +1,7 @@
1
1
  import ms from 'ms'
2
2
  import getSenderNonceKey from './get-sender-nonce-key'
3
3
 
4
- const UNCONFIRMED_TX_LIMIT = ms('5m')
4
+ export const UNCONFIRMED_TX_LIMIT = ms('5m')
5
5
 
6
6
  const mapToObject = (map) => Object.fromEntries([...map.entries()]) // only for string keys
7
7
 
package/LICENSE.md DELETED
File without changes