@exodus/ethereum-api 9.0.2 → 10.0.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 +22 -0
- package/package.json +2 -2
- package/src/create-asset-utils.js +0 -10
- package/src/error-wrapper.js +9 -1
- package/src/eth-like-util.js +7 -2
- package/src/exodus-eth-server/index.js +0 -4
- package/src/get-balance-for-address.js +1 -1
- package/src/get-balances.js +23 -13
- package/src/index.js +0 -1
- package/src/nft-utils.js +6 -2
- package/src/server-based-fee-monitor.js +1 -1
- package/src/tx-log/clarity-truncated-history-monitor.js +31 -10
- package/src/tx-log/clarity-truncated-history-monitor.md +6 -2
- package/src/tx-log/index.js +0 -1
- package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +7 -1
- package/src/tx-log/monitor-utils/index.js +0 -2
- package/src/exodus-eth-server/api.js +0 -276
- package/src/exodus-eth-server/ws.js +0 -126
- package/src/tx-log/ethereum-monitor.js +0 -351
- package/src/tx-log/monitor-utils/get-history-from-server.js +0 -43
- package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +0 -203
- package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +0 -40
- package/src/tx-log/ws-updates.js +0 -71
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
import { BaseMonitor } from '@exodus/asset-lib'
|
|
2
|
-
import { getAssetAddresses } from '@exodus/ethereum-lib'
|
|
3
|
-
import lodash from 'lodash'
|
|
4
|
-
|
|
5
|
-
import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
|
|
6
|
-
import { fromHexToString } from '../number-utils.js'
|
|
7
|
-
import {
|
|
8
|
-
checkPendingTransactions,
|
|
9
|
-
excludeUnchangedTokenBalances,
|
|
10
|
-
getAllLogItemsByAsset,
|
|
11
|
-
getDeriveDataNeededForTick,
|
|
12
|
-
getDeriveTransactionsToCheck,
|
|
13
|
-
getHistoryFromServer,
|
|
14
|
-
getLogItemsFromServerTx,
|
|
15
|
-
isUnverifiablePendingTxExpired,
|
|
16
|
-
verifyRpcPendingTxStatusBatch,
|
|
17
|
-
} from './monitor-utils/index.js'
|
|
18
|
-
import {
|
|
19
|
-
enableWSUpdates,
|
|
20
|
-
subscribeToGasPriceNotifications,
|
|
21
|
-
subscribeToWSNotifications,
|
|
22
|
-
} from './ws-updates.js'
|
|
23
|
-
|
|
24
|
-
const { isEmpty } = lodash
|
|
25
|
-
|
|
26
|
-
// The base ethereum monitor class handles listening for, fetching,
|
|
27
|
-
// formatting, and populating-to-state all ETH/ETC/ERC20 transactions.
|
|
28
|
-
|
|
29
|
-
export class EthereumMonitor extends BaseMonitor {
|
|
30
|
-
constructor({ server, config, rpcBalanceAssetNames, webSocketEnabled = true, ...args }) {
|
|
31
|
-
super(args)
|
|
32
|
-
this.server = server
|
|
33
|
-
this.config = { GAS_PRICE_FROM_WEBSOCKET: true, ...config }
|
|
34
|
-
this.deriveDataNeededForTick = getDeriveDataNeededForTick(this.aci)
|
|
35
|
-
this.deriveTransactionsToCheck = getDeriveTransactionsToCheck({
|
|
36
|
-
getTxLog: (...args) => this.aci.getTxLog(...args),
|
|
37
|
-
})
|
|
38
|
-
this.subscribedToWalletAccountNotificationsMap = new Map()
|
|
39
|
-
this.addHook('before-start', (...args) => this.beforeStart(...args))
|
|
40
|
-
this.addHook('after-stop', (...args) => this.afterStop(...args))
|
|
41
|
-
this.subscribedToGasPriceMap = new Map()
|
|
42
|
-
this._webSocketEnabled = webSocketEnabled
|
|
43
|
-
this.rpcBalanceAssetNames = rpcBalanceAssetNames
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
setServer(config = {}) {
|
|
47
|
-
const uri = config?.serverv1 || this.server.defaultUri
|
|
48
|
-
|
|
49
|
-
if (uri === this.server.getURL()) {
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
this.server.setURL(uri)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async deriveData({ assetSource, tokens }) {
|
|
57
|
-
const { asset: assetName, walletAccount } = assetSource
|
|
58
|
-
|
|
59
|
-
const { ourWalletAddress, currentAccountState, minimumConfirmations } =
|
|
60
|
-
await this.deriveDataNeededForTick({ assetName, walletAccount })
|
|
61
|
-
const transactionsToCheck = await this.deriveTransactionsToCheck({
|
|
62
|
-
assetName,
|
|
63
|
-
walletAccount,
|
|
64
|
-
tokens,
|
|
65
|
-
ourWalletAddress,
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
ourWalletAddress,
|
|
70
|
-
currentAccountState,
|
|
71
|
-
minimumConfirmations,
|
|
72
|
-
...transactionsToCheck,
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// eslint-disable-next-line no-undef
|
|
77
|
-
async checkPendingTransactions(params) {
|
|
78
|
-
const {
|
|
79
|
-
pendingTransactionsToCheck,
|
|
80
|
-
pendingTransactionsGroupedByAddressAndNonce,
|
|
81
|
-
recentPendingTransactions,
|
|
82
|
-
} = checkPendingTransactions(params)
|
|
83
|
-
const txsToRemove = []
|
|
84
|
-
const {
|
|
85
|
-
assetSource: { walletAccount },
|
|
86
|
-
} = params
|
|
87
|
-
|
|
88
|
-
const updateTx = (tx, asset, { isTxConfirmed, remove, preserve }) => {
|
|
89
|
-
if (remove) {
|
|
90
|
-
txsToRemove.push({ tx, assetSource: { asset, walletAccount } })
|
|
91
|
-
} else if (preserve) {
|
|
92
|
-
params.logItemsByAsset[asset].push(tx)
|
|
93
|
-
} else {
|
|
94
|
-
params.logItemsByAsset[asset].push({
|
|
95
|
-
...tx,
|
|
96
|
-
...(isTxConfirmed ? { confirmations: 1 } : { dropped: true, error: 'Dropped' }),
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// in case this is an ETH fee tx that has associated ERC20 send txs
|
|
101
|
-
const promises = tx.tokens.map(async (assetName) => {
|
|
102
|
-
const tokenTxSet = await this.aci.getTxLog({ assetName, walletAccount })
|
|
103
|
-
if (remove) {
|
|
104
|
-
txsToRemove.push({
|
|
105
|
-
tx: tokenTxSet.get(tx.txId),
|
|
106
|
-
assetSource: { asset: assetName, walletAccount },
|
|
107
|
-
})
|
|
108
|
-
} else if (tokenTxSet && tokenTxSet.has(tx.txId)) {
|
|
109
|
-
// Optional: the stored tx may name a token since removed from the registry
|
|
110
|
-
params.logItemsByAsset[assetName]?.push(
|
|
111
|
-
preserve
|
|
112
|
-
? tokenTxSet.get(tx.txId)
|
|
113
|
-
: {
|
|
114
|
-
...tokenTxSet.get(tx.txId),
|
|
115
|
-
...(isTxConfirmed ? { confirmations: 1 } : { dropped: true, error: 'Dropped' }),
|
|
116
|
-
}
|
|
117
|
-
)
|
|
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
|
-
delete recentPendingTransactions[tx.txId]
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Batch verify all pending txs with a single RPC call
|
|
135
|
-
const txIds = Object.keys(pendingTransactionsToCheck)
|
|
136
|
-
const statuses = await verifyRpcPendingTxStatusBatch({
|
|
137
|
-
server: this.server,
|
|
138
|
-
logger: this.logger,
|
|
139
|
-
txIds,
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
for (const { tx, assetName } of Object.values(pendingTransactionsToCheck)) {
|
|
143
|
-
const txStatus = statuses[tx.txId]
|
|
144
|
-
if (txStatus?.status === 'confirmed') {
|
|
145
|
-
await updateTx(tx, assetName, { isTxConfirmed: true })
|
|
146
|
-
} else if (txStatus?.status === 'dropped') {
|
|
147
|
-
if (params.refresh) {
|
|
148
|
-
await updateTx(tx, assetName, { remove: true })
|
|
149
|
-
} else {
|
|
150
|
-
await updateTx(tx, assetName, { isTxConfirmed: false })
|
|
151
|
-
}
|
|
152
|
-
} else if (params.refresh) {
|
|
153
|
-
// No txStatus at all means the whole verification batch failed - evidence
|
|
154
|
-
// about nothing, so preserve unconditionally; the age ceiling only applies
|
|
155
|
-
// to a per-entry 'unknown' verdict (evidence about that one hash).
|
|
156
|
-
if (!txStatus || txStatus.status === 'pending' || !isUnverifiablePendingTxExpired(tx)) {
|
|
157
|
-
// Still in the mempool (or not verifiable yet) but missing from the history
|
|
158
|
-
// server response: re-add it so the refresh overwrite cannot wipe it and
|
|
159
|
-
// break tx-log-based nonce resolution.
|
|
160
|
-
await updateTx(tx, assetName, { preserve: true })
|
|
161
|
-
} else {
|
|
162
|
-
// Unverifiable past the age ceiling: purge so a hash the node keeps
|
|
163
|
-
// erroring on cannot pin nonce derivation forever.
|
|
164
|
-
await updateTx(tx, assetName, { remove: true })
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
// non-refresh: 'pending' or unknown - tx still in mempool, do nothing
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (params.refresh) {
|
|
171
|
-
// Too recent for a reliable on-chain verdict - always survive the refresh overwrite
|
|
172
|
-
for (const { tx, assetName } of Object.values(recentPendingTransactions)) {
|
|
173
|
-
await updateTx(tx, assetName, { preserve: true })
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
txsToRemove,
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async tick({ refresh, walletAccount }) {
|
|
183
|
-
await this._subscribeToWSNotifications() // checks if it's a new walletAccount
|
|
184
|
-
|
|
185
|
-
const assets = await this.aci.getAssetsForNetwork({ baseAssetName: this.asset.name })
|
|
186
|
-
const tokens = Object.values(assets).filter((asset) => asset.baseAsset.name !== asset.name)
|
|
187
|
-
const tokensByAddress = tokens.reduce((map, token) => {
|
|
188
|
-
const addresses = getAssetAddresses(token)
|
|
189
|
-
for (const address of addresses) map.set(address.toLowerCase(), token)
|
|
190
|
-
return map
|
|
191
|
-
}, new Map())
|
|
192
|
-
const assetSource = { asset: this.asset.name, walletAccount }
|
|
193
|
-
|
|
194
|
-
const derivedData = await this.deriveData({ assetSource, tokens })
|
|
195
|
-
const derivedIndex = derivedData.currentAccountState?.index
|
|
196
|
-
|
|
197
|
-
const { allTransactionsFromServer, index } = await getHistoryFromServer({
|
|
198
|
-
server: this.server,
|
|
199
|
-
ourWalletAddress: derivedData.ourWalletAddress,
|
|
200
|
-
minimumConfirmations: derivedData.minimumConfirmations,
|
|
201
|
-
index: refresh ? 0 : (derivedIndex ?? 0),
|
|
202
|
-
})
|
|
203
|
-
const hasNewIndex = !derivedIndex || index > derivedIndex
|
|
204
|
-
|
|
205
|
-
const logItemsByAsset = getAllLogItemsByAsset({
|
|
206
|
-
getLogItemsFromServerTx,
|
|
207
|
-
ourWalletAddress: derivedData.ourWalletAddress,
|
|
208
|
-
allTransactionsFromServer,
|
|
209
|
-
asset: this.asset,
|
|
210
|
-
tokensByAddress,
|
|
211
|
-
assets,
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
const { txsToRemove } = await this.checkPendingTransactions({
|
|
215
|
-
txlist: allTransactionsFromServer,
|
|
216
|
-
assetSource,
|
|
217
|
-
refresh,
|
|
218
|
-
logItemsByAsset,
|
|
219
|
-
asset: this.asset,
|
|
220
|
-
...derivedData,
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
const accountState = await this.getNewAccountState({
|
|
224
|
-
tokens,
|
|
225
|
-
currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
|
|
226
|
-
ourWalletAddress: derivedData.ourWalletAddress,
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
const newData = { ...accountState, ...(hasNewIndex && { index }) }
|
|
230
|
-
await this.updateAccountState({ newData, walletAccount })
|
|
231
|
-
await this.removeFromTxLog(txsToRemove)
|
|
232
|
-
await this.updateTxLogByAsset({ logItemsByAsset, walletAccount, refresh })
|
|
233
|
-
|
|
234
|
-
if (refresh || hasNewIndex) {
|
|
235
|
-
const unknownTokenAddresses = this.getUnknownTokenAddresses({
|
|
236
|
-
transactions: allTransactionsFromServer,
|
|
237
|
-
tokensByAddress,
|
|
238
|
-
})
|
|
239
|
-
if (unknownTokenAddresses.length > 0) {
|
|
240
|
-
this.emit('unknown-tokens', unknownTokenAddresses)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async updateGasPrice(fetchedGasPrices) {
|
|
246
|
-
try {
|
|
247
|
-
await executeEthLikeFeeMonitorUpdate({
|
|
248
|
-
assetClientInterface: this.aci,
|
|
249
|
-
feeAsset: this.asset,
|
|
250
|
-
fetchedGasPrices,
|
|
251
|
-
})
|
|
252
|
-
} catch (e) {
|
|
253
|
-
this.logger.warn('error updating gasPrice', e)
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async getNewAccountState({ tokens, currentTokenBalances, ourWalletAddress }) {
|
|
258
|
-
const asset = this.asset
|
|
259
|
-
const newAccountState = Object.create(null)
|
|
260
|
-
const server = this.server
|
|
261
|
-
if (this.rpcBalanceAssetNames.includes(asset.name)) {
|
|
262
|
-
const result = await server.getBalanceProxied(ourWalletAddress)
|
|
263
|
-
const balance = fromHexToString(result)
|
|
264
|
-
newAccountState.balance = asset.currency.baseUnit(balance)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const tokenBalancePairs = await Promise.all(
|
|
268
|
-
tokens
|
|
269
|
-
.filter((token) => this.rpcBalanceAssetNames.includes(token.name) && token.contract.address)
|
|
270
|
-
.map(async (token) => {
|
|
271
|
-
const { confirmed } = await server.balanceOf(ourWalletAddress, token.contract.address)
|
|
272
|
-
const value = token.currency.baseUnit(confirmed[token.contract.address] || 0)
|
|
273
|
-
return [token.name, value]
|
|
274
|
-
})
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
const tokenBalances = excludeUnchangedTokenBalances(currentTokenBalances, tokenBalancePairs)
|
|
278
|
-
if (!isEmpty(tokenBalances)) newAccountState.tokenBalances = tokenBalances
|
|
279
|
-
return newAccountState
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
async getReceiveAddressesByWalletAccount() {
|
|
283
|
-
const walletAccounts = await this.aci.getWalletAccounts({ assetName: this.asset.name })
|
|
284
|
-
const addressesByAccount = Object.create(null)
|
|
285
|
-
for (const walletAccount of walletAccounts) {
|
|
286
|
-
addressesByAccount[walletAccount] = await this.aci.getReceiveAddresses({
|
|
287
|
-
assetName: this.asset.name,
|
|
288
|
-
walletAccount,
|
|
289
|
-
useCache: true,
|
|
290
|
-
})
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return addressesByAccount
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async _subscribeToWSNotifications() {
|
|
297
|
-
if (this._webSocketEnabled === false) return
|
|
298
|
-
|
|
299
|
-
const addressesByWalletAccount = await this.getReceiveAddressesByWalletAccount()
|
|
300
|
-
subscribeToWSNotifications({
|
|
301
|
-
addressesByWalletAccount,
|
|
302
|
-
tick: (...args) => this.tick(...args),
|
|
303
|
-
server: this.server,
|
|
304
|
-
})
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
getUnknownTokenAddresses({ transactions, tokensByAddress }) {
|
|
308
|
-
const set = transactions.reduce((acc, txn) => {
|
|
309
|
-
const transfers = txn.erc20 || []
|
|
310
|
-
transfers.forEach((transfer) => {
|
|
311
|
-
const addr = transfer.address.toLowerCase()
|
|
312
|
-
if (!tokensByAddress.has(addr)) {
|
|
313
|
-
acc.add(addr)
|
|
314
|
-
}
|
|
315
|
-
})
|
|
316
|
-
return acc
|
|
317
|
-
}, new Set())
|
|
318
|
-
return [...set]
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
async beforeStart() {
|
|
322
|
-
await this._enableWSUpdates()
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
async _enableWSUpdates() {
|
|
326
|
-
if (this._webSocketEnabled === false) return
|
|
327
|
-
|
|
328
|
-
const addressesByWalletAccount = await this.getReceiveAddressesByWalletAccount()
|
|
329
|
-
enableWSUpdates({
|
|
330
|
-
interval: this.interval,
|
|
331
|
-
server: this.server,
|
|
332
|
-
timer: this.timer,
|
|
333
|
-
tick: (...args) => this.tick(...args),
|
|
334
|
-
tickWalletAccounts: () => this.tickWalletAccounts(),
|
|
335
|
-
addressesByWalletAccount,
|
|
336
|
-
beforeStart: true,
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
if (this.config.GAS_PRICE_FROM_WEBSOCKET) {
|
|
340
|
-
subscribeToGasPriceNotifications({
|
|
341
|
-
server: this.server,
|
|
342
|
-
updateGasPrice: (newGasPrice) => this.updateGasPrice(newGasPrice),
|
|
343
|
-
subscribedToGasPriceMap: this.subscribedToGasPriceMap,
|
|
344
|
-
})
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
async afterStop() {
|
|
349
|
-
await this.server.stop()
|
|
350
|
-
}
|
|
351
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import isConfirmedServerTx from './is-confirmed-server-tx.js'
|
|
2
|
-
|
|
3
|
-
// Fetches JSON transaction history from the given Ethereum server
|
|
4
|
-
// object (from @exodus/ethereum-api). Starts fetching from the given
|
|
5
|
-
// block cursor and returns the latest cursor and all transactions
|
|
6
|
-
// fetched in the process. minimumConfirmations determines how many
|
|
7
|
-
// confirmations a transaction must have for the cursor to be moved
|
|
8
|
-
// to that transaction's block.
|
|
9
|
-
|
|
10
|
-
export default async function getHistoryFromServer({
|
|
11
|
-
server,
|
|
12
|
-
ourWalletAddress,
|
|
13
|
-
index,
|
|
14
|
-
minimumConfirmations,
|
|
15
|
-
limit = 1000,
|
|
16
|
-
}) {
|
|
17
|
-
if (limit === 0) return []
|
|
18
|
-
const allTransactionsFromServer = []
|
|
19
|
-
|
|
20
|
-
let isEndOfAddressHistory = false
|
|
21
|
-
let newIndex = index
|
|
22
|
-
while (true) {
|
|
23
|
-
const transactions = await server.getHistoryV2(ourWalletAddress, {
|
|
24
|
-
index,
|
|
25
|
-
limit, // the server returns the number of TXs that's less or equal to the limit
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
if (transactions.length === 0) isEndOfAddressHistory = true
|
|
29
|
-
for (const transaction of transactions) {
|
|
30
|
-
allTransactionsFromServer.push(transaction)
|
|
31
|
-
index = transaction.addressIndex + 1 // this index is used to fetch a next page of TXs
|
|
32
|
-
if (transaction.confirmations >= minimumConfirmations) newIndex = transaction.addressIndex + 1 // this index is used to return from the function
|
|
33
|
-
if (!isConfirmedServerTx(transaction)) isEndOfAddressHistory = true
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (isEndOfAddressHistory) {
|
|
37
|
-
return {
|
|
38
|
-
allTransactionsFromServer,
|
|
39
|
-
index: newIndex,
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import lodash from 'lodash'
|
|
2
|
-
|
|
3
|
-
import getFeeAmount from './get-fee-amount.js'
|
|
4
|
-
import getNamesOfTokensTransferredByServerTx from './get-names-of-tokens-transferred-by-server-tx.js'
|
|
5
|
-
import getRawTransferAmount from './get-raw-transfer-amount.js'
|
|
6
|
-
import getTransfersByTokenName from './get-transfers-by-token-name.js'
|
|
7
|
-
import getValueOfTransfers from './get-value-of-transfers.js'
|
|
8
|
-
import isConfirmedServerTx from './is-confirmed-server-tx.js'
|
|
9
|
-
import isConsideredSentTokenTx from './is-considered-sent-token-tx.js'
|
|
10
|
-
|
|
11
|
-
// This function takes a server transaction object fetched from magnifier,
|
|
12
|
-
// and transforms it into Tx models to update the exodus state.
|
|
13
|
-
|
|
14
|
-
const tryFindExternalRecipient = (transfers, ourWalletAddress) =>
|
|
15
|
-
transfers.find(({ to }) => to !== ourWalletAddress)?.to || ourWalletAddress
|
|
16
|
-
|
|
17
|
-
function isSelfSendTx({
|
|
18
|
-
coinAmount,
|
|
19
|
-
ourWalletWasSender,
|
|
20
|
-
sendingTransferPresent,
|
|
21
|
-
receivingTransferPresent,
|
|
22
|
-
}) {
|
|
23
|
-
return (
|
|
24
|
-
coinAmount.isZero && sendingTransferPresent && receivingTransferPresent && ourWalletWasSender
|
|
25
|
-
)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isNftTransfer({ serverTx, ourWalletAddress }) {
|
|
29
|
-
if (!Array.isArray(serverTx.erc721) || serverTx.erc721.length === 0) return false
|
|
30
|
-
|
|
31
|
-
return serverTx.erc721.some(
|
|
32
|
-
(transfer) => transfer.to === ourWalletAddress || transfer.from === ourWalletAddress
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export default function getLogItemsFromServerTx({
|
|
37
|
-
serverTx,
|
|
38
|
-
asset,
|
|
39
|
-
ourWalletAddress,
|
|
40
|
-
tokensByAddress,
|
|
41
|
-
assets,
|
|
42
|
-
}) {
|
|
43
|
-
const confirmations = isConfirmedServerTx(serverTx) ? 1 : 0
|
|
44
|
-
const date = new Date(parseInt(serverTx.timestamp, 16) * 1000) // included even for unconfirmed txs
|
|
45
|
-
const txId = serverTx.hash
|
|
46
|
-
const nonce = parseInt(serverTx.nonce, 16)
|
|
47
|
-
const gasLimit = parseInt(serverTx.gas, 16)
|
|
48
|
-
const error = serverTx.error || (serverTx.status === false ? 'Failed' : null)
|
|
49
|
-
const feeAmount = getFeeAmount(asset, serverTx)
|
|
50
|
-
const ethereumTransfers = [serverTx, ...(serverTx.internal || [])]
|
|
51
|
-
const tokenTransfersByTokenName = getTransfersByTokenName(serverTx.erc20 || [], tokensByAddress)
|
|
52
|
-
const toAddress = tryFindExternalRecipient(ethereumTransfers, ourWalletAddress)
|
|
53
|
-
const ourWalletWasSender = serverTx.from === ourWalletAddress
|
|
54
|
-
const METHOD_ID_LENGTH = 10
|
|
55
|
-
const methodId = serverTx.data && {
|
|
56
|
-
methodId: serverTx.data.slice(0, Math.max(0, METHOD_ID_LENGTH)),
|
|
57
|
-
}
|
|
58
|
-
const data = serverTx.data || '0x'
|
|
59
|
-
const logItemCommonProperties = {
|
|
60
|
-
confirmations,
|
|
61
|
-
date,
|
|
62
|
-
error,
|
|
63
|
-
txId,
|
|
64
|
-
dropped: false,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const logItemsForServerTxEntries = []
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
const sendingTransferPresent = ethereumTransfers.some(({ from }) => from === ourWalletAddress)
|
|
71
|
-
const receivingTransferPresent = ethereumTransfers.some(({ to }) => to === ourWalletAddress)
|
|
72
|
-
const nftTransferPresent = isNftTransfer({ serverTx, ourWalletAddress })
|
|
73
|
-
|
|
74
|
-
if (sendingTransferPresent || receivingTransferPresent || nftTransferPresent) {
|
|
75
|
-
const coinAmount = getValueOfTransfers(ourWalletAddress, asset, ethereumTransfers)
|
|
76
|
-
// `rawCoinAmount` mirrors the field written by `getOptimisticTxLogEffects`
|
|
77
|
-
// and is consumed by the bump path (`tx-create.js`) to recover the
|
|
78
|
-
// user-signed on-chain `value` when accelerating a self-send (where
|
|
79
|
-
// `coinAmount` is zero). For the base-asset entry the raw amount is the
|
|
80
|
-
// top-level `value` field of the tx itself, not the sum of internal
|
|
81
|
-
// transfers — that's what the user signed and what we must restore on
|
|
82
|
-
// bump.
|
|
83
|
-
const rawCoinAmount = getRawTransferAmount({
|
|
84
|
-
ourWalletAddress,
|
|
85
|
-
asset,
|
|
86
|
-
transfers: [serverTx],
|
|
87
|
-
}).toBaseString()
|
|
88
|
-
const selfSend = isSelfSendTx({
|
|
89
|
-
coinAmount,
|
|
90
|
-
ourWalletWasSender,
|
|
91
|
-
sendingTransferPresent,
|
|
92
|
-
receivingTransferPresent,
|
|
93
|
-
})
|
|
94
|
-
let sent
|
|
95
|
-
if (sendingTransferPresent) {
|
|
96
|
-
sent = serverTx.internal
|
|
97
|
-
.map(({ from, to, value }) => {
|
|
98
|
-
if (from === ourWalletAddress) {
|
|
99
|
-
const amount = asset.currency.baseUnit(value).toDefaultString({ unit: true })
|
|
100
|
-
return { address: to, amount }
|
|
101
|
-
}
|
|
102
|
-
})
|
|
103
|
-
.filter(Boolean)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
logItemsForServerTxEntries.push([
|
|
107
|
-
asset.name,
|
|
108
|
-
{
|
|
109
|
-
...logItemCommonProperties,
|
|
110
|
-
coinAmount,
|
|
111
|
-
coinName: asset.name,
|
|
112
|
-
data: {
|
|
113
|
-
data,
|
|
114
|
-
nonce,
|
|
115
|
-
gasLimit,
|
|
116
|
-
rawCoinAmount,
|
|
117
|
-
...methodId,
|
|
118
|
-
...(sent?.length > 0 ? { sent } : undefined),
|
|
119
|
-
},
|
|
120
|
-
...(ourWalletWasSender
|
|
121
|
-
? { from: [], to: toAddress, feeAmount, feeCoinName: asset.feeAsset.name }
|
|
122
|
-
: { from: [serverTx.from] }),
|
|
123
|
-
selfSend,
|
|
124
|
-
tokens: getNamesOfTokensTransferredByServerTx({
|
|
125
|
-
asset,
|
|
126
|
-
tokensByAddress,
|
|
127
|
-
serverTx,
|
|
128
|
-
ourWalletAddress,
|
|
129
|
-
}),
|
|
130
|
-
currencies: {
|
|
131
|
-
[asset.name]: asset.currency,
|
|
132
|
-
[asset.feeAsset.name]: asset.feeAsset.currency,
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
])
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// handle erc20
|
|
140
|
-
Object.entries(tokenTransfersByTokenName).forEach(([tokenName, tokenTransfers]) => {
|
|
141
|
-
const sendingTransferPresent = tokenTransfers.some(({ from }) => from === ourWalletAddress)
|
|
142
|
-
const receivingTransferPresent = tokenTransfers.some(({ to }) => to === ourWalletAddress)
|
|
143
|
-
if (!sendingTransferPresent && !receivingTransferPresent) return
|
|
144
|
-
|
|
145
|
-
const token = assets[tokenName]
|
|
146
|
-
const tokenTransferToAddress = tryFindExternalRecipient(tokenTransfers, ourWalletAddress)
|
|
147
|
-
// Transfers from this reader's server source are tagged with
|
|
148
|
-
// `events: true` once the tx is included in a block (event-log derived)
|
|
149
|
-
// and `events: false` while still pending (tx-data derived). Only one
|
|
150
|
-
// flavor is real for a given lifecycle state; filtering avoids
|
|
151
|
-
// double-counting if both ever coexist for the same transfer. Both
|
|
152
|
-
// `coinAmount` and `rawCoinAmount` must see the same set so they stay
|
|
153
|
-
// consistent.
|
|
154
|
-
const filteredTokenTransfers = lodash.filter(tokenTransfers, {
|
|
155
|
-
events: confirmations > 0,
|
|
156
|
-
})
|
|
157
|
-
const coinAmount = getValueOfTransfers(ourWalletAddress, token, filteredTokenTransfers)
|
|
158
|
-
// See base-entry comment above; for token entries the raw amount is the
|
|
159
|
-
// absolute total tokens our wallet moved in this tx (sum of outgoing, or
|
|
160
|
-
// incoming for receive-only entries).
|
|
161
|
-
const rawCoinAmount = getRawTransferAmount({
|
|
162
|
-
ourWalletAddress,
|
|
163
|
-
asset: token,
|
|
164
|
-
transfers: filteredTokenTransfers,
|
|
165
|
-
}).toBaseString()
|
|
166
|
-
const tokenFromAddresses = lodash.uniq(
|
|
167
|
-
tokenTransfers.filter(({ to }) => to === ourWalletAddress).map(({ from }) => from)
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
const isConsideredSent = isConsideredSentTokenTx({
|
|
171
|
-
coinAmount,
|
|
172
|
-
ourWalletWasSender,
|
|
173
|
-
sendingTransferPresent,
|
|
174
|
-
receivingTransferPresent,
|
|
175
|
-
})
|
|
176
|
-
const selfSend = isSelfSendTx({
|
|
177
|
-
coinAmount,
|
|
178
|
-
ourWalletWasSender,
|
|
179
|
-
sendingTransferPresent,
|
|
180
|
-
receivingTransferPresent,
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
logItemsForServerTxEntries.push([
|
|
184
|
-
tokenName,
|
|
185
|
-
{
|
|
186
|
-
...logItemCommonProperties,
|
|
187
|
-
coinAmount,
|
|
188
|
-
coinName: tokenName,
|
|
189
|
-
data: { data, nonce, gasLimit, rawCoinAmount, ...methodId },
|
|
190
|
-
...(isConsideredSent
|
|
191
|
-
? { from: [], to: tokenTransferToAddress, feeAmount, feeCoinName: asset.feeAsset.name }
|
|
192
|
-
: { from: tokenFromAddresses }),
|
|
193
|
-
selfSend,
|
|
194
|
-
currencies: {
|
|
195
|
-
[token.name]: token.currency,
|
|
196
|
-
[token.feeAsset.name]: token.feeAsset.currency,
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
])
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
return Object.fromEntries(logItemsForServerTxEntries)
|
|
203
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { FEE_PAYMENT_PREFIX } from '@exodus/ethereum-lib'
|
|
2
|
-
import lodash from 'lodash'
|
|
3
|
-
|
|
4
|
-
import getValueOfTransfers from './get-value-of-transfers.js'
|
|
5
|
-
|
|
6
|
-
// For ETH transactions, we store an array of token names inside
|
|
7
|
-
// the TX model. This array is used to display text in the UI for
|
|
8
|
-
// transactions which pay fees for ERC20 token transfers. This
|
|
9
|
-
// function is used to determine that array, choosing which
|
|
10
|
-
// tokens a transaction ostensibly 'paid fees' for.
|
|
11
|
-
|
|
12
|
-
export default function getNamesOfTokensTransferredByServerTx({
|
|
13
|
-
asset,
|
|
14
|
-
tokensByAddress,
|
|
15
|
-
ourWalletAddress,
|
|
16
|
-
serverTx,
|
|
17
|
-
}) {
|
|
18
|
-
// Treat smart contract ETH transfer transactions as ETH transfers, not token transfers
|
|
19
|
-
if (!getValueOfTransfers(ourWalletAddress, asset, serverTx.internal || []).isZero) {
|
|
20
|
-
return []
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const tokenAddresses = (serverTx.erc20 || []).map((event) => event.address)
|
|
24
|
-
if (
|
|
25
|
-
tokenAddresses.length === 0 &&
|
|
26
|
-
serverTx.data &&
|
|
27
|
-
serverTx.data !== '0x' &&
|
|
28
|
-
!serverTx.data.startsWith(FEE_PAYMENT_PREFIX)
|
|
29
|
-
) {
|
|
30
|
-
// We may still be transacting with tokens even though we found no `tokenAddresses`. This includes
|
|
31
|
-
// failed token transactions as well as other transactions sent to a token contract. If we do not
|
|
32
|
-
// recognize the `to` address in the next step, then we will not treat it as a token contract address.
|
|
33
|
-
tokenAddresses.push(serverTx.to)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return lodash
|
|
37
|
-
.uniq(tokenAddresses)
|
|
38
|
-
.map((address) => tokensByAddress.get(address)?.name)
|
|
39
|
-
.filter(Boolean)
|
|
40
|
-
}
|