@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.
@@ -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
- }