@exodus/ethereum-api 2.8.2 → 2.8.3
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 +10 -3
- package/src/eth-like-util.js +7 -0
- package/src/exodus-eth-server/api.js +6 -0
- package/src/exodus-eth-server/ws.js +18 -8
- package/src/index.js +4 -0
- package/src/tx-log/__tests__/asset-client-interface-test-helper.js +22 -0
- package/src/tx-log/__tests__/assets-for-test-helper.js +30 -0
- package/src/tx-log/__tests__/bsc-history-return-values-for-test-helper.js +94 -0
- package/src/tx-log/__tests__/bsc-monitor.integration.test.js +166 -0
- package/src/tx-log/__tests__/bsc-monitor.test.js +184 -0
- package/src/tx-log/__tests__/ethereum-history-return-values-for-test-helper.js +357 -0
- package/src/tx-log/__tests__/ethereum-monitor.integration.test.js +174 -0
- package/src/tx-log/__tests__/ethereum-monitor.test.js +160 -0
- package/src/tx-log/__tests__/in-memory-asset-client-interface.js +196 -0
- package/src/tx-log/ethereum-monitor.js +271 -0
- package/src/tx-log/index.js +1 -0
- package/src/tx-log/ws-updates.js +75 -0
- package/src/websocket/index.js +23 -3
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { pickBy, isEmpty, partition, isObject, bindAll, functions } from 'lodash'
|
|
2
|
+
import assert from 'minimalistic-assert'
|
|
3
|
+
import { Tx, TxSet } from '@exodus/models'
|
|
4
|
+
import { EventEmitter } from 'events'
|
|
5
|
+
|
|
6
|
+
const getKey = ({ assetName, walletAccount }) => `${walletAccount}:${assetName}`
|
|
7
|
+
const getAssetOnlyKey = ({ assetName }) => assetName
|
|
8
|
+
|
|
9
|
+
const createTxSet = ({ txs, coinName, assets }) => {
|
|
10
|
+
const values = txs.map((tx) => Tx.fromJSON({ ...tx, date: tx.date || new Date(), coinName }))
|
|
11
|
+
return TxSet.fromArray(values, { assets })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const createBatch = () => {
|
|
15
|
+
const batch = []
|
|
16
|
+
const push = (promise) => batch.push(promise)
|
|
17
|
+
const exec = async () => {
|
|
18
|
+
// enforce sequential execution
|
|
19
|
+
for (const promise in batch) {
|
|
20
|
+
await promise
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return { push, exec }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class InMemoryAssetClientInterface extends EventEmitter {
|
|
27
|
+
constructor({ assets, logger, walletAccounts, wallet }) {
|
|
28
|
+
super()
|
|
29
|
+
assert(assets, 'assets is required')
|
|
30
|
+
assert(logger, 'logger is required')
|
|
31
|
+
assert(walletAccounts, 'walletAccounts is required')
|
|
32
|
+
assert(wallet, 'wallet is required')
|
|
33
|
+
assert(isObject(assets), 'assets must be an object indexed by asset name')
|
|
34
|
+
this.assets = assets
|
|
35
|
+
this.logger = logger
|
|
36
|
+
this.wallet = wallet
|
|
37
|
+
this.feeData = Object.values(assets)
|
|
38
|
+
.filter((asset) => asset.name === asset.baseAsset.name)
|
|
39
|
+
.filter((asset) => asset.api?.getFeeData)
|
|
40
|
+
.reduce((acc, asset) => ({ ...acc, [asset.name]: asset.api.getFeeData() }), {})
|
|
41
|
+
this.walletAccounts = walletAccounts
|
|
42
|
+
this.txs = {}
|
|
43
|
+
this.states = {}
|
|
44
|
+
bindAll(this, functions(this))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async getTxLog({ assetName, walletAccount }) {
|
|
48
|
+
const key = getKey({ assetName, walletAccount })
|
|
49
|
+
if (!this.txs[key]) {
|
|
50
|
+
return TxSet.EMPTY
|
|
51
|
+
}
|
|
52
|
+
return this.txs[key]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async updateTxLogAndNotify({ assetName, walletAccount, txs }) {
|
|
56
|
+
const batch = this.updateTxLogAndNotifyBatch({
|
|
57
|
+
assetName,
|
|
58
|
+
walletAccount,
|
|
59
|
+
txs,
|
|
60
|
+
})
|
|
61
|
+
return batch.exec()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
updateTxLogAndNotifyBatch({ assetName, walletAccount, txs, batch = createBatch() }) {
|
|
65
|
+
batch.push(this.updateTxs({ assetName, walletAccount, txs }))
|
|
66
|
+
return batch
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async updateTxs({ assetName, walletAccount, txs }) {
|
|
70
|
+
const currentTxLog = await this.getTxLog({ assetName, walletAccount })
|
|
71
|
+
const [existingLogItems, newLogItems] = partition(txs, (v) => currentTxLog.get(v.txId))
|
|
72
|
+
let newTxSet = currentTxLog
|
|
73
|
+
if (existingLogItems.length > 0) {
|
|
74
|
+
newTxSet = newTxSet.updateTxsProperties(existingLogItems)
|
|
75
|
+
}
|
|
76
|
+
if (newLogItems.length > 0) {
|
|
77
|
+
newTxSet = newTxSet.union(
|
|
78
|
+
createTxSet({ txs: newLogItems, coinName: assetName, assets: this.assets })
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
const key = getKey({ assetName, walletAccount })
|
|
82
|
+
if (newTxSet.size > 0) {
|
|
83
|
+
this.txs[key] = newTxSet
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getAccountState({ assetName, walletAccount }) {
|
|
88
|
+
const asset = this.assets[assetName]
|
|
89
|
+
const key = getKey({ assetName, walletAccount })
|
|
90
|
+
|
|
91
|
+
if (!this.states[key]) {
|
|
92
|
+
const accountState = asset.api?.createAccountState?.()?.create()
|
|
93
|
+
if (!accountState) {
|
|
94
|
+
throw new Error(`Asset ${asset.name} does not support account state!`)
|
|
95
|
+
}
|
|
96
|
+
this.states[key] = accountState
|
|
97
|
+
}
|
|
98
|
+
return this.states[key]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async updateAccountState({ assetName, walletAccount, newData }) {
|
|
102
|
+
this.updateAccountStateBatch({
|
|
103
|
+
assetName,
|
|
104
|
+
walletAccount,
|
|
105
|
+
newData,
|
|
106
|
+
}).exec()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
updateAccountStateBatch({ assetName, walletAccount, newData, batch = createBatch() }) {
|
|
110
|
+
batch.push(
|
|
111
|
+
this._basicUpdateAccountState({
|
|
112
|
+
assetName,
|
|
113
|
+
walletAccount,
|
|
114
|
+
newData,
|
|
115
|
+
})
|
|
116
|
+
)
|
|
117
|
+
return batch
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async _basicUpdateAccountState({ assetName, walletAccount, newData }) {
|
|
121
|
+
if (!isEmpty(newData)) {
|
|
122
|
+
const key = getKey({ assetName, walletAccount })
|
|
123
|
+
const currentAccountState = await this.getAccountState({ assetName, walletAccount })
|
|
124
|
+
const newAccountState = currentAccountState.merge(newData)
|
|
125
|
+
this.states[key] = newAccountState
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async getReceiveAddresses({ assetName, walletAccount, chainIndex, addressIndex }) {
|
|
130
|
+
return [await this.getReceiveAddress({ assetName, walletAccount, chainIndex, addressIndex })]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async getReceiveAddress({ assetName, walletAccount, chainIndex, addressIndex }) {
|
|
134
|
+
const asset = this.assets[assetName]
|
|
135
|
+
const baseAsset = asset.baseAsset
|
|
136
|
+
assert(this.walletAccounts[walletAccount], 'Invalid walletAccount')
|
|
137
|
+
const accountIndex = this.walletAccounts[walletAccount].index
|
|
138
|
+
const { publicKey } = await this.wallet.genPublicKey({
|
|
139
|
+
baseAssetName: baseAsset.name,
|
|
140
|
+
accountIndex,
|
|
141
|
+
chainIndex,
|
|
142
|
+
addressIndex,
|
|
143
|
+
})
|
|
144
|
+
return asset.keys.encodePublic(publicKey).toString()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async getWalletAccountsByAsset() {
|
|
148
|
+
const baseAssets = Object.values(this.assets).filter(
|
|
149
|
+
(asset) => asset.name === asset.baseAsset.name
|
|
150
|
+
)
|
|
151
|
+
const walletAccountMap = {}
|
|
152
|
+
for (const baseAsset of baseAssets) {
|
|
153
|
+
walletAccountMap[baseAsset.name] = Object.keys(this.walletAccounts)
|
|
154
|
+
}
|
|
155
|
+
return walletAccountMap
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async getWalletAccounts({ assetName }) {
|
|
159
|
+
const walletAccountMap = await this.getWalletAccountsByAsset()
|
|
160
|
+
const walletAccounts = walletAccountMap[assetName]
|
|
161
|
+
assert(walletAccounts, `${assetName} is not supported`)
|
|
162
|
+
return walletAccounts
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async getAssetsForNetwork({ baseAssetName }) {
|
|
166
|
+
return pickBy(this.assets, (asset) => asset.baseAsset.name === baseAssetName)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async fetchToken({ assetId, baseAssetName }) {
|
|
170
|
+
throw new Error('implement!')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async addToken({ assetId, baseAssetName, allowedStatusList }) {
|
|
174
|
+
throw new Error('Implement!')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async getConfirmationsNumber({ assetName }) {
|
|
178
|
+
const baseAsset = this.assets[assetName].baseAsset
|
|
179
|
+
return baseAsset.api?.getConfirmationsNumber ? baseAsset.api.getConfirmationsNumber() : 1
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async getFeeData({ assetName }) {
|
|
183
|
+
const feeData = this.feeData[getAssetOnlyKey({ assetName })]
|
|
184
|
+
if (!feeData) {
|
|
185
|
+
throw new Error(`Asset ${assetName} does not support fee data`)
|
|
186
|
+
}
|
|
187
|
+
return feeData
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async updateFeeConfig({ assetName, feeConfig }) {
|
|
191
|
+
const currentFeeData = await this.getFeeData({ assetName })
|
|
192
|
+
const newFeeData = currentFeeData.update(feeConfig)
|
|
193
|
+
this.feeData[getAssetOnlyKey({ assetName })] = newFeeData
|
|
194
|
+
this.emit('fee-config-updated')
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
import { getServer } from '@exodus/ethereum-api'
|
|
3
|
+
import {
|
|
4
|
+
type PendingTransactionsDictionary,
|
|
5
|
+
checkPendingTransactions,
|
|
6
|
+
getAllLogItemsByAsset,
|
|
7
|
+
getDeriveDataNeededForTick,
|
|
8
|
+
getDeriveTransactionsToCheck,
|
|
9
|
+
getHistoryFromServer,
|
|
10
|
+
isRpcBalanceAsset,
|
|
11
|
+
getAssetAddresses,
|
|
12
|
+
} from '@exodus/ethereum-lib'
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
enableWSUpdates,
|
|
16
|
+
subscribeToGasPriceNotifications,
|
|
17
|
+
subscribeToWSNotifications,
|
|
18
|
+
} from './ws-updates'
|
|
19
|
+
|
|
20
|
+
import { isEmpty } from 'lodash'
|
|
21
|
+
|
|
22
|
+
import { type Tx } from '@exodus/models'
|
|
23
|
+
|
|
24
|
+
import { BaseMonitor } from '@exodus/asset-lib'
|
|
25
|
+
|
|
26
|
+
export type { AssetSource } from '@exodus/models/lib/types'
|
|
27
|
+
|
|
28
|
+
type DerivedData = {
|
|
29
|
+
ourWalletAddress: string,
|
|
30
|
+
currentAccountState: Object,
|
|
31
|
+
minimumConfirmations: number,
|
|
32
|
+
unconfirmedTransactions: PendingTransactionsDictionary,
|
|
33
|
+
pendingTransactionsGroupedByAddressAndNonce: PendingTransactionsDictionary,
|
|
34
|
+
simulatedTransactions: any,
|
|
35
|
+
}
|
|
36
|
+
// The base ethereum monitor class handles listening for, fetching,
|
|
37
|
+
// formatting, and populating-to-state all ETH/ETC/ERC20 transactions.
|
|
38
|
+
|
|
39
|
+
export class EthereumMonitor extends BaseMonitor {
|
|
40
|
+
constructor({ server, config, ...args }) {
|
|
41
|
+
super(args)
|
|
42
|
+
this.server = server || getServer(this.asset)
|
|
43
|
+
this.config = { GAS_PRICE_FROM_WEBSOCKET: true, ...config }
|
|
44
|
+
this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
|
|
45
|
+
this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
|
|
46
|
+
getTxLog: (...args) => this.aci.getTxLog(...args),
|
|
47
|
+
})
|
|
48
|
+
this.subscribedToWalletAccountNotificationsMap = new Map()
|
|
49
|
+
this.addHook('before-start', (...args) => this.beforeStart(...args))
|
|
50
|
+
this.addHook('after-stop', (...args) => this.afterStop(...args))
|
|
51
|
+
this.subscribedToGasPriceMap = new Map()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setServer(config = {}) {
|
|
55
|
+
if (config.serverv1 && this.server.getURL() !== config.serverv1) {
|
|
56
|
+
this.server.setURL(config.serverv1)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async deriveData({ assetSource, tokens }: Object): DerivedData {
|
|
61
|
+
const { asset: assetName, walletAccount } = assetSource
|
|
62
|
+
|
|
63
|
+
const {
|
|
64
|
+
ourWalletAddress,
|
|
65
|
+
currentAccountState,
|
|
66
|
+
minimumConfirmations,
|
|
67
|
+
} = await this.deriveDataNeededForTick({ assetName, walletAccount })
|
|
68
|
+
const transactionsToCheck = await this.deriveTransactionsToCheck({
|
|
69
|
+
assetName,
|
|
70
|
+
walletAccount,
|
|
71
|
+
tokens,
|
|
72
|
+
ourWalletAddress,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
ourWalletAddress,
|
|
77
|
+
currentAccountState,
|
|
78
|
+
minimumConfirmations,
|
|
79
|
+
...transactionsToCheck,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// eslint-disable-next-line no-undef
|
|
84
|
+
async checkPendingTransactions(params): { txsToRemove: { tx: Tx, assetSource: AssetSource }[] } {
|
|
85
|
+
const {
|
|
86
|
+
pendingTransactionsToCheck,
|
|
87
|
+
pendingTransactionsGroupedByAddressAndNonce,
|
|
88
|
+
} = checkPendingTransactions(params)
|
|
89
|
+
const txsToRemove = []
|
|
90
|
+
const {
|
|
91
|
+
assetSource: { walletAccount },
|
|
92
|
+
} = params
|
|
93
|
+
|
|
94
|
+
const updateTx = (tx, asset, { error, remove }) => {
|
|
95
|
+
if (remove) {
|
|
96
|
+
txsToRemove.push({ tx, assetSource: { asset, walletAccount } })
|
|
97
|
+
} else {
|
|
98
|
+
params.logItemsByAsset[asset].push({
|
|
99
|
+
...tx,
|
|
100
|
+
dropped: true,
|
|
101
|
+
error,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// in case this is an ETH fee tx that has associated ERC20 send txs
|
|
106
|
+
const promises = tx.tokens.map(async (assetName) => {
|
|
107
|
+
const tokenTxSet = await this.aci.getTxLog({ assetName, walletAccount })
|
|
108
|
+
if (remove) {
|
|
109
|
+
txsToRemove.push({
|
|
110
|
+
tx: tokenTxSet.get(tx.txId),
|
|
111
|
+
assetSource: { asset: assetName, walletAccount },
|
|
112
|
+
})
|
|
113
|
+
} else if (tokenTxSet && tokenTxSet.has(tx.txId)) {
|
|
114
|
+
params.logItemsByAsset[assetName].push({
|
|
115
|
+
...tokenTxSet.get(tx.txId),
|
|
116
|
+
error,
|
|
117
|
+
dropped: true,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
return Promise.all(promises)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const { tx, assetName, replaced = false } of Object.values(
|
|
125
|
+
pendingTransactionsGroupedByAddressAndNonce
|
|
126
|
+
)) {
|
|
127
|
+
if (replaced) {
|
|
128
|
+
await updateTx(tx, assetName, { remove: true })
|
|
129
|
+
delete pendingTransactionsToCheck[tx.txId]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const { tx, assetName } of Object.values(pendingTransactionsToCheck)) {
|
|
134
|
+
if (params.refresh) await updateTx(tx, assetName, { remove: true })
|
|
135
|
+
else await updateTx(tx, assetName, { error: 'Dropped' })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
txsToRemove,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async tick({ refresh, walletAccount }) {
|
|
144
|
+
await this._subscribeToWSNotifications() // checks if it's a new walletAccount
|
|
145
|
+
|
|
146
|
+
const assets = await this.aci.getAssetsForNetwork({ baseAssetName: this.asset.name })
|
|
147
|
+
const tokens = Object.values(assets).filter((asset) => asset.baseAsset.name !== asset.name)
|
|
148
|
+
const tokensByAddress = tokens.reduce((map, token) => {
|
|
149
|
+
const addresses = getAssetAddresses(token)
|
|
150
|
+
for (const address of addresses) map.set(address.toLowerCase(), token)
|
|
151
|
+
return map
|
|
152
|
+
}, new Map())
|
|
153
|
+
const assetSource = { asset: this.asset.name, walletAccount }
|
|
154
|
+
|
|
155
|
+
const derivedData = await this.deriveData({ assetSource, tokens })
|
|
156
|
+
|
|
157
|
+
const { allTransactionsFromServer, index } = await getHistoryFromServer({
|
|
158
|
+
server: this.server,
|
|
159
|
+
ourWalletAddress: derivedData.ourWalletAddress,
|
|
160
|
+
minimumConfirmations: derivedData.minimumConfirmations,
|
|
161
|
+
index: refresh ? 0 : derivedData.currentAccountState?.index ?? 0,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const logItemsByAsset = getAllLogItemsByAsset({
|
|
165
|
+
ourWalletAddress: derivedData.ourWalletAddress,
|
|
166
|
+
allTransactionsFromServer,
|
|
167
|
+
asset: this.asset,
|
|
168
|
+
tokensByAddress,
|
|
169
|
+
assets,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const { txsToRemove } = await this.checkPendingTransactions({
|
|
173
|
+
txlist: allTransactionsFromServer,
|
|
174
|
+
assetSource,
|
|
175
|
+
refresh,
|
|
176
|
+
logItemsByAsset,
|
|
177
|
+
asset: this.asset,
|
|
178
|
+
...derivedData,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const accountState = await this.getNewAccountState({
|
|
182
|
+
tokens,
|
|
183
|
+
ourWalletAddress: derivedData.ourWalletAddress,
|
|
184
|
+
})
|
|
185
|
+
await this.updateAccountState({ newData: { index, ...accountState }, walletAccount })
|
|
186
|
+
|
|
187
|
+
await this.removeFromTxLog(txsToRemove)
|
|
188
|
+
await this.updateTxLogByAsset({ logItemsByAsset, walletAccount, refresh })
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async updateGasPrice(newGasPrice) {
|
|
192
|
+
try {
|
|
193
|
+
const feeConfig = { gasPrice: `${parseInt(newGasPrice, 16)} wei` }
|
|
194
|
+
this.logger.debug(`Update ${this.asset.name} gas price: ${feeConfig.gasPrice}`)
|
|
195
|
+
await this.aci.updateFeeConfig({ assetName: this.asset.name, feeConfig })
|
|
196
|
+
} catch (e) {
|
|
197
|
+
this.logger.warn('error updating gasPrice', e)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async getNewAccountState({ tokens, ourWalletAddress }) {
|
|
202
|
+
const asset = this.asset
|
|
203
|
+
const newAccountState = {}
|
|
204
|
+
const server = this.server
|
|
205
|
+
if (isRpcBalanceAsset(asset)) {
|
|
206
|
+
const { confirmed } = await server.getBalance(ourWalletAddress)
|
|
207
|
+
newAccountState.balance = asset.currency.baseUnit(confirmed.value)
|
|
208
|
+
}
|
|
209
|
+
const tokenBalances = Object.assign(
|
|
210
|
+
{},
|
|
211
|
+
...(await Promise.all(
|
|
212
|
+
tokens
|
|
213
|
+
.filter((token) => isRpcBalanceAsset(token) && token.contract.address)
|
|
214
|
+
.map(async (token) => {
|
|
215
|
+
const { confirmed } = await server.balanceOf(ourWalletAddress, token.contract.address)
|
|
216
|
+
const value = confirmed[token.contract.address]
|
|
217
|
+
const balance = token.currency.baseUnit(value || 0)
|
|
218
|
+
return { [token.name]: balance }
|
|
219
|
+
})
|
|
220
|
+
))
|
|
221
|
+
)
|
|
222
|
+
if (!isEmpty(tokenBalances)) newAccountState.tokenBalances = tokenBalances
|
|
223
|
+
return newAccountState
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async getReceiveAddressesByWalletAccount() {
|
|
227
|
+
const walletAccounts = await this.aci.getWalletAccounts({ assetName: this.asset.name })
|
|
228
|
+
const addressesByAccount = {}
|
|
229
|
+
for (const walletAccount of walletAccounts) {
|
|
230
|
+
addressesByAccount[walletAccount] = await this.aci.getReceiveAddresses({
|
|
231
|
+
assetName: this.asset.name,
|
|
232
|
+
walletAccount,
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
return addressesByAccount
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async _subscribeToWSNotifications() {
|
|
239
|
+
const addressesByWalletAccount = await this.getReceiveAddressesByWalletAccount()
|
|
240
|
+
subscribeToWSNotifications({
|
|
241
|
+
addressesByWalletAccount,
|
|
242
|
+
tick: (...args) => this.tick(...args),
|
|
243
|
+
server: this.server,
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async beforeStart() {
|
|
248
|
+
const addressesByWalletAccount = await this.getReceiveAddressesByWalletAccount()
|
|
249
|
+
enableWSUpdates({
|
|
250
|
+
interval: this.interval,
|
|
251
|
+
server: this.server,
|
|
252
|
+
timer: this.timer,
|
|
253
|
+
tick: (...args) => this.tick(...args),
|
|
254
|
+
tickAllWalletAccounts: () => this.tickAllWalletAccounts(),
|
|
255
|
+
addressesByWalletAccount,
|
|
256
|
+
beforeStart: true,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
if (this.config.GAS_PRICE_FROM_WEBSOCKET) {
|
|
260
|
+
subscribeToGasPriceNotifications({
|
|
261
|
+
server: this.server,
|
|
262
|
+
updateGasPrice: (newGasPrice) => this.updateGasPrice(newGasPrice),
|
|
263
|
+
subscribedToGasPriceMap: this.subscribedToGasPriceMap,
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async afterStop() {
|
|
269
|
+
await this.server.stop()
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ethereum-monitor'
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import ms from 'ms'
|
|
2
|
+
|
|
3
|
+
const subscribedToAddressNotificationsMap = new Map()
|
|
4
|
+
|
|
5
|
+
const UPDATE_LOOP_INTERVAL = ms('5m')
|
|
6
|
+
|
|
7
|
+
export function subscribeToWSNotifications({
|
|
8
|
+
addressesByWalletAccount,
|
|
9
|
+
tick,
|
|
10
|
+
getState,
|
|
11
|
+
server,
|
|
12
|
+
beforeStart = false,
|
|
13
|
+
}) {
|
|
14
|
+
Object.entries(addressesByWalletAccount).forEach(([walletAccount, addresses]) => {
|
|
15
|
+
const address = String(Array.from(addresses)[0]).toLowerCase() // Only check m/0/0
|
|
16
|
+
const mapKey = `${server.getURL()}:${address}` // different chains might have the same addresses
|
|
17
|
+
if (subscribedToAddressNotificationsMap.has(mapKey)) return
|
|
18
|
+
|
|
19
|
+
server.ws.watch(address)
|
|
20
|
+
server.ws.events.on(`address-${address}`, () => {
|
|
21
|
+
tick({ refresh: false, getState, walletAccount })
|
|
22
|
+
})
|
|
23
|
+
subscribedToAddressNotificationsMap.set(mapKey, true)
|
|
24
|
+
if (!beforeStart) {
|
|
25
|
+
// tick for a new wallet account
|
|
26
|
+
tick({ refresh: false, getState, walletAccount })
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function subscribeToGasPriceNotifications({
|
|
32
|
+
server,
|
|
33
|
+
updateGasPrice,
|
|
34
|
+
subscribedToGasPriceMap,
|
|
35
|
+
}) {
|
|
36
|
+
const mapKey = server.getURL()
|
|
37
|
+
if (subscribedToGasPriceMap.has(mapKey)) return
|
|
38
|
+
server.ws.events.on('gasprice', updateGasPrice)
|
|
39
|
+
subscribedToGasPriceMap.set(mapKey, true)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function enableWSUpdates({
|
|
43
|
+
interval,
|
|
44
|
+
server,
|
|
45
|
+
timer,
|
|
46
|
+
dispatch,
|
|
47
|
+
getState,
|
|
48
|
+
tickAllWalletAccounts,
|
|
49
|
+
tick,
|
|
50
|
+
addressesByWalletAccount,
|
|
51
|
+
beforeStart = false,
|
|
52
|
+
}) {
|
|
53
|
+
const defaultInterval = interval
|
|
54
|
+
|
|
55
|
+
subscribeToWSNotifications({
|
|
56
|
+
server,
|
|
57
|
+
getState,
|
|
58
|
+
beforeStart,
|
|
59
|
+
tick,
|
|
60
|
+
addressesByWalletAccount,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
server.ws.events.on('open', () => {
|
|
64
|
+
if (timer.isRunning) {
|
|
65
|
+
timer.setNewInterval(UPDATE_LOOP_INTERVAL)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
server.ws.events.on('close', async () => {
|
|
69
|
+
if (timer.isRunning) {
|
|
70
|
+
await timer.setNewInterval(defaultInterval)
|
|
71
|
+
return tickAllWalletAccounts({ dispatch, getState })
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
server.ws.open()
|
|
75
|
+
}
|
package/src/websocket/index.js
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* This is a "light" version of @exodus/core resolution to select the native WebSocket on BE
|
|
3
|
+
* It's a workaround until https://github.com/ExodusMovement/assets/pull/72 is merged.
|
|
4
|
+
*
|
|
5
|
+
* Based on https://github.com/ExodusMovement/fetch/blob/master/core.js , note that chooses native on Desktop
|
|
6
|
+
*/
|
|
7
|
+
if (
|
|
8
|
+
typeof process !== 'undefined' &&
|
|
9
|
+
process &&
|
|
10
|
+
(process.type === 'renderer' || process.type === 'worker')
|
|
11
|
+
) {
|
|
12
|
+
// THIS IS FOR DESKTOP
|
|
13
|
+
module.exports = require('ws')
|
|
14
|
+
} else {
|
|
15
|
+
// eslint-disable-next-line no-undef
|
|
16
|
+
if (global.window?.WebSocket) {
|
|
17
|
+
// THIS IS FOR BE
|
|
18
|
+
module.exports = global.window?.WebSocket
|
|
19
|
+
} else {
|
|
20
|
+
// THIS IS FOR UNIT TESTING.
|
|
21
|
+
module.exports = require('ws')
|
|
22
|
+
}
|
|
23
|
+
}
|