github.com/status-im/status-go@v1.1.0/services/wallet/transfer/commands.go (about)

     1  package transfer
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"math/big"
     7  	"time"
     8  
     9  	"golang.org/x/exp/maps"
    10  
    11  	"github.com/ethereum/go-ethereum/common"
    12  	"github.com/ethereum/go-ethereum/core/types"
    13  	"github.com/ethereum/go-ethereum/event"
    14  	"github.com/ethereum/go-ethereum/log"
    15  
    16  	"github.com/status-im/status-go/rpc/chain"
    17  	"github.com/status-im/status-go/services/wallet/async"
    18  	"github.com/status-im/status-go/services/wallet/balance"
    19  	w_common "github.com/status-im/status-go/services/wallet/common"
    20  	"github.com/status-im/status-go/services/wallet/token"
    21  	"github.com/status-im/status-go/services/wallet/walletevent"
    22  	"github.com/status-im/status-go/transactions"
    23  )
    24  
    25  const (
    26  	// EventNewTransfers emitted when new block was added to the same canonical chan.
    27  	EventNewTransfers walletevent.EventType = "new-transfers"
    28  	// EventFetchingRecentHistory emitted when fetching of lastest tx history is started
    29  	EventFetchingRecentHistory walletevent.EventType = "recent-history-fetching"
    30  	// EventRecentHistoryReady emitted when fetching of lastest tx history is started
    31  	EventRecentHistoryReady walletevent.EventType = "recent-history-ready"
    32  	// EventFetchingHistoryError emitted when fetching of tx history failed
    33  	EventFetchingHistoryError walletevent.EventType = "fetching-history-error"
    34  	// EventNonArchivalNodeDetected emitted when a connection to a non archival node is detected
    35  	EventNonArchivalNodeDetected walletevent.EventType = "non-archival-node-detected"
    36  
    37  	// Internal events emitted when different kinds of transfers are detected
    38  	EventInternalETHTransferDetected     walletevent.EventType = walletevent.InternalEventTypePrefix + "eth-transfer-detected"
    39  	EventInternalERC20TransferDetected   walletevent.EventType = walletevent.InternalEventTypePrefix + "erc20-transfer-detected"
    40  	EventInternalERC721TransferDetected  walletevent.EventType = walletevent.InternalEventTypePrefix + "erc721-transfer-detected"
    41  	EventInternalERC1155TransferDetected walletevent.EventType = walletevent.InternalEventTypePrefix + "erc1155-transfer-detected"
    42  
    43  	numberOfBlocksCheckedPerIteration = 40
    44  	noBlockLimit                      = 0
    45  )
    46  
    47  var (
    48  	// This will work only for binance testnet as mainnet doesn't support
    49  	// archival request.
    50  	binanceChainErc20BatchSize    = big.NewInt(5000)
    51  	goerliErc20BatchSize          = big.NewInt(100000)
    52  	goerliErc20ArbitrumBatchSize  = big.NewInt(10000)
    53  	goerliErc20OptimismBatchSize  = big.NewInt(10000)
    54  	sepoliaErc20BatchSize         = big.NewInt(100000)
    55  	sepoliaErc20ArbitrumBatchSize = big.NewInt(10000)
    56  	sepoliaErc20OptimismBatchSize = big.NewInt(10000)
    57  	erc20BatchSize                = big.NewInt(100000)
    58  
    59  	transfersRetryInterval = 5 * time.Second
    60  )
    61  
    62  type ethHistoricalCommand struct {
    63  	address       common.Address
    64  	chainClient   chain.ClientInterface
    65  	balanceCacher balance.Cacher
    66  	feed          *event.Feed
    67  	foundHeaders  []*DBHeader
    68  	error         error
    69  	noLimit       bool
    70  
    71  	from                          *Block
    72  	to, resultingFrom, startBlock *big.Int
    73  	threadLimit                   uint32
    74  }
    75  
    76  type Transaction []*Transfer
    77  
    78  func (c *ethHistoricalCommand) Command() async.Command {
    79  	return async.FiniteCommand{
    80  		Interval: 5 * time.Second,
    81  		Runable:  c.Run,
    82  	}.Run
    83  }
    84  
    85  func (c *ethHistoricalCommand) Run(ctx context.Context) (err error) {
    86  	log.Debug("eth historical downloader start", "chainID", c.chainClient.NetworkID(), "address", c.address,
    87  		"from", c.from.Number, "to", c.to, "noLimit", c.noLimit)
    88  
    89  	start := time.Now()
    90  	if c.from.Number != nil && c.from.Balance != nil {
    91  		c.balanceCacher.Cache().AddBalance(c.address, c.chainClient.NetworkID(), c.from.Number, c.from.Balance)
    92  	}
    93  	if c.from.Number != nil && c.from.Nonce != nil {
    94  		c.balanceCacher.Cache().AddNonce(c.address, c.chainClient.NetworkID(), c.from.Number, c.from.Nonce)
    95  	}
    96  	from, headers, startBlock, err := findBlocksWithEthTransfers(ctx, c.chainClient,
    97  		c.balanceCacher, c.address, c.from.Number, c.to, c.noLimit, c.threadLimit)
    98  
    99  	if err != nil {
   100  		c.error = err
   101  		log.Error("failed to find blocks with transfers", "error", err, "chainID", c.chainClient.NetworkID(),
   102  			"address", c.address, "from", c.from.Number, "to", c.to)
   103  		return nil
   104  	}
   105  
   106  	c.foundHeaders = headers
   107  	c.resultingFrom = from
   108  	c.startBlock = startBlock
   109  
   110  	log.Debug("eth historical downloader finished successfully", "chain", c.chainClient.NetworkID(),
   111  		"address", c.address, "from", from, "to", c.to, "total blocks", len(headers), "time", time.Since(start))
   112  
   113  	return nil
   114  }
   115  
   116  type erc20HistoricalCommand struct {
   117  	erc20       BatchDownloader
   118  	chainClient chain.ClientInterface
   119  	feed        *event.Feed
   120  
   121  	iterator     *IterativeDownloader
   122  	to           *big.Int
   123  	from         *big.Int
   124  	foundHeaders []*DBHeader
   125  }
   126  
   127  func (c *erc20HistoricalCommand) Command() async.Command {
   128  	return async.FiniteCommand{
   129  		Interval: 5 * time.Second,
   130  		Runable:  c.Run,
   131  	}.Run
   132  }
   133  
   134  func getErc20BatchSize(chainID uint64) *big.Int {
   135  	switch chainID {
   136  	case w_common.EthereumSepolia:
   137  		return sepoliaErc20BatchSize
   138  	case w_common.OptimismSepolia:
   139  		return sepoliaErc20OptimismBatchSize
   140  	case w_common.ArbitrumSepolia:
   141  		return sepoliaErc20ArbitrumBatchSize
   142  	case w_common.EthereumGoerli:
   143  		return goerliErc20BatchSize
   144  	case w_common.OptimismGoerli:
   145  		return goerliErc20OptimismBatchSize
   146  	case w_common.ArbitrumGoerli:
   147  		return goerliErc20ArbitrumBatchSize
   148  	case w_common.BinanceChainID:
   149  		return binanceChainErc20BatchSize
   150  	case w_common.BinanceTestChainID:
   151  		return binanceChainErc20BatchSize
   152  	default:
   153  		return erc20BatchSize
   154  	}
   155  }
   156  
   157  func (c *erc20HistoricalCommand) Run(ctx context.Context) (err error) {
   158  	log.Debug("wallet historical downloader for erc20 transfers start", "chainID", c.chainClient.NetworkID(),
   159  		"from", c.from, "to", c.to)
   160  
   161  	start := time.Now()
   162  	if c.iterator == nil {
   163  		c.iterator, err = SetupIterativeDownloader(
   164  			c.chainClient,
   165  			c.erc20, getErc20BatchSize(c.chainClient.NetworkID()), c.to, c.from)
   166  		if err != nil {
   167  			log.Error("failed to setup historical downloader for erc20")
   168  			return err
   169  		}
   170  	}
   171  	for !c.iterator.Finished() {
   172  		headers, _, _, err := c.iterator.Next(ctx)
   173  		if err != nil {
   174  			log.Error("failed to get next batch", "error", err, "chainID", c.chainClient.NetworkID()) // TODO: stop inifinite command in case of an error that we can't fix like missing trie node
   175  			return err
   176  		}
   177  		c.foundHeaders = append(c.foundHeaders, headers...)
   178  	}
   179  	log.Debug("wallet historical downloader for erc20 transfers finished", "chainID", c.chainClient.NetworkID(),
   180  		"from", c.from, "to", c.to, "time", time.Since(start), "headers", len(c.foundHeaders))
   181  	return nil
   182  }
   183  
   184  type transfersCommand struct {
   185  	db                 *Database
   186  	blockDAO           *BlockDAO
   187  	eth                *ETHDownloader
   188  	blockNums          []*big.Int
   189  	address            common.Address
   190  	chainClient        chain.ClientInterface
   191  	blocksLimit        int
   192  	transactionManager *TransactionManager
   193  	pendingTxManager   *transactions.PendingTxTracker
   194  	tokenManager       *token.Manager
   195  	feed               *event.Feed
   196  
   197  	// result
   198  	fetchedTransfers []Transfer
   199  }
   200  
   201  func (c *transfersCommand) Runner(interval ...time.Duration) async.Runner {
   202  	intvl := transfersRetryInterval
   203  	if len(interval) > 0 {
   204  		intvl = interval[0]
   205  	}
   206  	return async.FiniteCommandWithErrorCounter{
   207  		FiniteCommand: async.FiniteCommand{
   208  			Interval: intvl,
   209  			Runable:  c.Run,
   210  		},
   211  		ErrorCounter: async.NewErrorCounter(5, "transfersCommand"),
   212  	}
   213  }
   214  
   215  func (c *transfersCommand) Command(interval ...time.Duration) async.Command {
   216  	return c.Runner(interval...).Run
   217  }
   218  
   219  func (c *transfersCommand) Run(ctx context.Context) (err error) {
   220  	// Take blocks from cache if available and disrespect the limit
   221  	// If no blocks are available in cache, take blocks from DB respecting the limit
   222  	// If no limit is set, take all blocks from DB
   223  	log.Debug("start transfersCommand", "chain", c.chainClient.NetworkID(), "address", c.address, "blockNums", c.blockNums)
   224  	startTs := time.Now()
   225  
   226  	for {
   227  		blocks := c.blockNums
   228  		if blocks == nil {
   229  			blocks, _ = c.blockDAO.GetBlocksToLoadByAddress(c.chainClient.NetworkID(), c.address, numberOfBlocksCheckedPerIteration)
   230  		}
   231  
   232  		for _, blockNum := range blocks {
   233  			log.Debug("transfersCommand block start", "chain", c.chainClient.NetworkID(), "address", c.address, "block", blockNum)
   234  
   235  			allTransfers, err := c.eth.GetTransfersByNumber(ctx, blockNum)
   236  			if err != nil {
   237  				log.Error("getTransfersByBlocks error", "error", err)
   238  				return err
   239  			}
   240  
   241  			c.processUnknownErc20CommunityTransactions(ctx, allTransfers)
   242  
   243  			if len(allTransfers) > 0 {
   244  				// First, try to match to any pre-existing pending/multi-transaction
   245  				err := c.saveAndConfirmPending(allTransfers, blockNum)
   246  				if err != nil {
   247  					log.Error("saveAndConfirmPending error", "error", err)
   248  					return err
   249  				}
   250  
   251  				// Check if multi transaction needs to be created
   252  				err = c.processMultiTransactions(ctx, allTransfers)
   253  				if err != nil {
   254  					log.Error("processMultiTransactions error", "error", err)
   255  					return err
   256  				}
   257  			} else {
   258  				// If no transfers found, that is suspecting, because downloader returned this block as containing transfers
   259  				log.Error("no transfers found in block", "chain", c.chainClient.NetworkID(), "address", c.address, "block", blockNum)
   260  
   261  				err = markBlocksAsLoaded(c.chainClient.NetworkID(), c.db.client, c.address, []*big.Int{blockNum})
   262  				if err != nil {
   263  					log.Error("Mark blocks loaded error", "error", err)
   264  					return err
   265  				}
   266  			}
   267  
   268  			c.fetchedTransfers = append(c.fetchedTransfers, allTransfers...)
   269  
   270  			c.notifyOfNewTransfers(blockNum, allTransfers)
   271  			c.notifyOfLatestTransfers(allTransfers, w_common.EthTransfer)
   272  			c.notifyOfLatestTransfers(allTransfers, w_common.Erc20Transfer)
   273  			c.notifyOfLatestTransfers(allTransfers, w_common.Erc721Transfer)
   274  			c.notifyOfLatestTransfers(allTransfers, w_common.Erc1155Transfer)
   275  
   276  			log.Debug("transfersCommand block end", "chain", c.chainClient.NetworkID(), "address", c.address,
   277  				"block", blockNum, "tranfers.len", len(allTransfers), "fetchedTransfers.len", len(c.fetchedTransfers))
   278  		}
   279  
   280  		if c.blockNums != nil || len(blocks) == 0 ||
   281  			(c.blocksLimit > noBlockLimit && len(blocks) >= c.blocksLimit) {
   282  			log.Debug("loadTransfers breaking loop on block limits reached or 0 blocks", "chain", c.chainClient.NetworkID(),
   283  				"address", c.address, "limit", c.blocksLimit, "blocks", len(blocks))
   284  			break
   285  		}
   286  	}
   287  
   288  	log.Debug("end transfersCommand", "chain", c.chainClient.NetworkID(), "address", c.address,
   289  		"blocks.len", len(c.blockNums), "transfers.len", len(c.fetchedTransfers), "in", time.Since(startTs))
   290  
   291  	return nil
   292  }
   293  
   294  // saveAndConfirmPending ensures only the transaction that has owner (Address) as a sender is matched to the
   295  // corresponding multi-transaction (by multi-transaction ID). This way we ensure that if receiver is in the list
   296  // of accounts filter will discard the proper one
   297  func (c *transfersCommand) saveAndConfirmPending(allTransfers []Transfer, blockNum *big.Int) error {
   298  	tx, resErr := c.db.client.Begin()
   299  	if resErr != nil {
   300  		return resErr
   301  	}
   302  	notifyFunctions := c.confirmPendingTransactions(tx, allTransfers)
   303  	defer func() {
   304  		if resErr == nil {
   305  			commitErr := tx.Commit()
   306  			if commitErr != nil {
   307  				log.Error("failed to commit", "error", commitErr)
   308  			}
   309  			for _, notify := range notifyFunctions {
   310  				notify()
   311  			}
   312  		} else {
   313  			rollbackErr := tx.Rollback()
   314  			if rollbackErr != nil {
   315  				log.Error("failed to rollback", "error", rollbackErr)
   316  			}
   317  		}
   318  	}()
   319  
   320  	resErr = saveTransfersMarkBlocksLoaded(tx, c.chainClient.NetworkID(), c.address, allTransfers, []*big.Int{blockNum})
   321  	if resErr != nil {
   322  		log.Error("SaveTransfers error", "error", resErr)
   323  	}
   324  
   325  	return resErr
   326  }
   327  
   328  func externalTransactionOrError(err error, mTID int64) bool {
   329  	if err == sql.ErrNoRows {
   330  		// External transaction downloaded, ignore it
   331  		return true
   332  	} else if err != nil {
   333  		log.Warn("GetOwnedMultiTransactionID", "error", err)
   334  		return true
   335  	} else if mTID <= 0 {
   336  		// Existing external transaction, ignore it
   337  		return true
   338  	}
   339  	return false
   340  }
   341  
   342  func (c *transfersCommand) confirmPendingTransactions(tx *sql.Tx, allTransfers []Transfer) (notifyFunctions []func()) {
   343  	notifyFunctions = make([]func(), 0)
   344  
   345  	// Confirm all pending transactions that are included in this block
   346  	for i, tr := range allTransfers {
   347  		chainID := w_common.ChainID(tr.NetworkID)
   348  		txHash := tr.Receipt.TxHash
   349  		txType, mTID, err := transactions.GetOwnedPendingStatus(tx, chainID, txHash, tr.Address)
   350  		if err == sql.ErrNoRows {
   351  			if tr.MultiTransactionID > 0 {
   352  				continue
   353  			} else {
   354  				// Outside transaction, already confirmed by another duplicate or not yet downloaded
   355  				existingMTID, err := GetOwnedMultiTransactionID(tx, chainID, txHash, tr.Address)
   356  				if externalTransactionOrError(err, existingMTID) {
   357  					continue
   358  				}
   359  				mTID = w_common.NewAndSet(existingMTID)
   360  			}
   361  		} else if err != nil {
   362  			log.Warn("GetOwnedPendingStatus", "error", err)
   363  			continue
   364  		}
   365  
   366  		if mTID != nil {
   367  			allTransfers[i].MultiTransactionID = w_common.MultiTransactionIDType(*mTID)
   368  		}
   369  		if txType != nil && *txType == transactions.WalletTransfer {
   370  			notify, err := c.pendingTxManager.DeleteBySQLTx(tx, chainID, txHash)
   371  			if err != nil && err != transactions.ErrStillPending {
   372  				log.Error("DeleteBySqlTx error", "error", err)
   373  			}
   374  			notifyFunctions = append(notifyFunctions, notify)
   375  		}
   376  	}
   377  	return notifyFunctions
   378  }
   379  
   380  // Mark all subTxs of a given Tx with the same multiTxID
   381  func setMultiTxID(tx Transaction, multiTxID w_common.MultiTransactionIDType) {
   382  	for _, subTx := range tx {
   383  		subTx.MultiTransactionID = multiTxID
   384  	}
   385  }
   386  
   387  func (c *transfersCommand) markMultiTxTokensAsPreviouslyOwned(ctx context.Context, multiTransaction *MultiTransaction, ownerAddress common.Address) {
   388  	if multiTransaction == nil {
   389  		return
   390  	}
   391  	if len(multiTransaction.ToAsset) > 0 && multiTransaction.ToNetworkID > 0 {
   392  		token := c.tokenManager.GetToken(multiTransaction.ToNetworkID, multiTransaction.ToAsset)
   393  		_, _ = c.tokenManager.MarkAsPreviouslyOwnedToken(token, ownerAddress)
   394  	}
   395  	if len(multiTransaction.FromAsset) > 0 && multiTransaction.FromNetworkID > 0 {
   396  		token := c.tokenManager.GetToken(multiTransaction.FromNetworkID, multiTransaction.FromAsset)
   397  		_, _ = c.tokenManager.MarkAsPreviouslyOwnedToken(token, ownerAddress)
   398  	}
   399  }
   400  
   401  func (c *transfersCommand) checkAndProcessSwapMultiTx(ctx context.Context, tx Transaction) (bool, error) {
   402  	for _, subTx := range tx {
   403  		switch subTx.Type {
   404  		// If the Tx contains any uniswapV2Swap/uniswapV3Swap subTx, generate a Swap multiTx
   405  		case w_common.UniswapV2Swap, w_common.UniswapV3Swap:
   406  			multiTransaction, err := buildUniswapSwapMultitransaction(ctx, c.chainClient, c.tokenManager, subTx)
   407  			if err != nil {
   408  				return false, err
   409  			}
   410  
   411  			if multiTransaction != nil {
   412  				id, err := c.transactionManager.InsertMultiTransaction(multiTransaction)
   413  				if err != nil {
   414  					return false, err
   415  				}
   416  				setMultiTxID(tx, id)
   417  				c.markMultiTxTokensAsPreviouslyOwned(ctx, multiTransaction, subTx.Address)
   418  				return true, nil
   419  			}
   420  		}
   421  	}
   422  
   423  	return false, nil
   424  }
   425  
   426  func (c *transfersCommand) checkAndProcessBridgeMultiTx(ctx context.Context, tx Transaction) (bool, error) {
   427  	for _, subTx := range tx {
   428  		switch subTx.Type {
   429  		// If the Tx contains any hopBridge subTx, create/update Bridge multiTx
   430  		case w_common.HopBridgeFrom, w_common.HopBridgeTo:
   431  			multiTransaction, err := buildHopBridgeMultitransaction(ctx, c.chainClient, c.transactionManager, c.tokenManager, subTx)
   432  			if err != nil {
   433  				return false, err
   434  			}
   435  
   436  			if multiTransaction != nil {
   437  				setMultiTxID(tx, multiTransaction.ID)
   438  				c.markMultiTxTokensAsPreviouslyOwned(ctx, multiTransaction, subTx.Address)
   439  				return true, nil
   440  			}
   441  		}
   442  	}
   443  
   444  	return false, nil
   445  }
   446  
   447  func (c *transfersCommand) processUnknownErc20CommunityTransactions(ctx context.Context, allTransfers []Transfer) {
   448  	for _, tx := range allTransfers {
   449  		// To can be nil in case of erc20 contract creation
   450  		if tx.Type == w_common.Erc20Transfer && tx.Transaction.To() != nil {
   451  			// Find token in db or if this is a community token, find its metadata
   452  			token := c.tokenManager.FindOrCreateTokenByAddress(ctx, tx.NetworkID, *tx.Transaction.To())
   453  			if token != nil {
   454  				isFirst := false
   455  				if token.Verified || token.CommunityData != nil {
   456  					isFirst, _ = c.tokenManager.MarkAsPreviouslyOwnedToken(token, tx.Address)
   457  				}
   458  				if token.CommunityData != nil {
   459  					go c.tokenManager.SignalCommunityTokenReceived(tx.Address, tx.ID, tx.TokenValue, token, isFirst)
   460  				}
   461  			}
   462  		}
   463  	}
   464  }
   465  
   466  func (c *transfersCommand) processMultiTransactions(ctx context.Context, allTransfers []Transfer) error {
   467  	txByTxHash := subTransactionListToTransactionsByTxHash(allTransfers)
   468  
   469  	// Detect / Generate multitransactions
   470  	// Iterate over all detected transactions
   471  	for _, tx := range txByTxHash {
   472  		// Check if already matched to a multi transaction
   473  		if tx[0].MultiTransactionID > 0 {
   474  			continue
   475  		}
   476  
   477  		// Then check for a Swap transaction
   478  		txProcessed, err := c.checkAndProcessSwapMultiTx(ctx, tx)
   479  		if err != nil {
   480  			return err
   481  		}
   482  		if txProcessed {
   483  			continue
   484  		}
   485  
   486  		// Then check for a Bridge transaction
   487  		_, err = c.checkAndProcessBridgeMultiTx(ctx, tx)
   488  		if err != nil {
   489  			return err
   490  		}
   491  	}
   492  
   493  	return nil
   494  }
   495  
   496  func (c *transfersCommand) notifyOfNewTransfers(blockNum *big.Int, transfers []Transfer) {
   497  	if c.feed != nil {
   498  		if len(transfers) > 0 {
   499  			c.feed.Send(walletevent.Event{
   500  				Type:        EventNewTransfers,
   501  				Accounts:    []common.Address{c.address},
   502  				ChainID:     c.chainClient.NetworkID(),
   503  				BlockNumber: blockNum,
   504  			})
   505  		}
   506  	}
   507  }
   508  
   509  func transferTypeToEventType(transferType w_common.Type) walletevent.EventType {
   510  	switch transferType {
   511  	case w_common.EthTransfer:
   512  		return EventInternalETHTransferDetected
   513  	case w_common.Erc20Transfer:
   514  		return EventInternalERC20TransferDetected
   515  	case w_common.Erc721Transfer:
   516  		return EventInternalERC721TransferDetected
   517  	case w_common.Erc1155Transfer:
   518  		return EventInternalERC1155TransferDetected
   519  	default:
   520  		return ""
   521  	}
   522  }
   523  
   524  func (c *transfersCommand) notifyOfLatestTransfers(transfers []Transfer, transferType w_common.Type) {
   525  	if c.feed != nil {
   526  		eventTransfers := make([]Transfer, 0, len(transfers))
   527  		latestTransferTimestamp := uint64(0)
   528  		for _, transfer := range transfers {
   529  			if transfer.Type == transferType {
   530  				eventTransfers = append(eventTransfers, transfer)
   531  				if transfer.Timestamp > latestTransferTimestamp {
   532  					latestTransferTimestamp = transfer.Timestamp
   533  				}
   534  			}
   535  		}
   536  		if len(eventTransfers) > 0 {
   537  			c.feed.Send(walletevent.Event{
   538  				Type:        transferTypeToEventType(transferType),
   539  				Accounts:    []common.Address{c.address},
   540  				ChainID:     c.chainClient.NetworkID(),
   541  				At:          int64(latestTransferTimestamp),
   542  				EventParams: eventTransfers,
   543  			})
   544  		}
   545  	}
   546  }
   547  
   548  type loadTransfersCommand struct {
   549  	accounts           []common.Address
   550  	db                 *Database
   551  	blockDAO           *BlockDAO
   552  	chainClient        chain.ClientInterface
   553  	blocksByAddress    map[common.Address][]*big.Int
   554  	transactionManager *TransactionManager
   555  	pendingTxManager   *transactions.PendingTxTracker
   556  	blocksLimit        int
   557  	tokenManager       *token.Manager
   558  	feed               *event.Feed
   559  }
   560  
   561  func (c *loadTransfersCommand) Command() async.Command {
   562  	return async.FiniteCommand{
   563  		Interval: 5 * time.Second,
   564  		Runable:  c.Run,
   565  	}.Run
   566  }
   567  
   568  // This command always returns nil, even if there is an error in one of the commands.
   569  // `transferCommand`s retry until maxError, but this command doesn't retry.
   570  // In case some transfer is not loaded after max retries, it will be retried only after restart of the app.
   571  // Currently there is no implementation to keep retrying until success. I think this should be implemented
   572  // in `transferCommand` with exponential backoff instead of `loadTransfersCommand` (issue #4608).
   573  func (c *loadTransfersCommand) Run(parent context.Context) (err error) {
   574  	return loadTransfers(parent, c.blockDAO, c.db, c.chainClient, c.blocksLimit, c.blocksByAddress,
   575  		c.transactionManager, c.pendingTxManager, c.tokenManager, c.feed)
   576  }
   577  
   578  func loadTransfers(ctx context.Context, blockDAO *BlockDAO, db *Database,
   579  	chainClient chain.ClientInterface, blocksLimitPerAccount int, blocksByAddress map[common.Address][]*big.Int,
   580  	transactionManager *TransactionManager, pendingTxManager *transactions.PendingTxTracker,
   581  	tokenManager *token.Manager, feed *event.Feed) error {
   582  
   583  	log.Debug("loadTransfers start", "chain", chainClient.NetworkID(), "limit", blocksLimitPerAccount)
   584  
   585  	start := time.Now()
   586  	group := async.NewGroup(ctx)
   587  
   588  	accounts := maps.Keys(blocksByAddress)
   589  	for _, address := range accounts {
   590  		transfers := &transfersCommand{
   591  			db:          db,
   592  			blockDAO:    blockDAO,
   593  			chainClient: chainClient,
   594  			address:     address,
   595  			eth: &ETHDownloader{
   596  				chainClient: chainClient,
   597  				accounts:    []common.Address{address},
   598  				signer:      types.LatestSignerForChainID(chainClient.ToBigInt()),
   599  				db:          db,
   600  			},
   601  			blockNums:          blocksByAddress[address],
   602  			transactionManager: transactionManager,
   603  			pendingTxManager:   pendingTxManager,
   604  			tokenManager:       tokenManager,
   605  			feed:               feed,
   606  		}
   607  		group.Add(transfers.Command())
   608  	}
   609  
   610  	select {
   611  	case <-ctx.Done():
   612  		log.Debug("loadTransfers cancelled", "chain", chainClient.NetworkID(), "error", ctx.Err())
   613  	case <-group.WaitAsync():
   614  		log.Debug("loadTransfers finished for account", "in", time.Since(start), "chain", chainClient.NetworkID())
   615  	}
   616  	return nil
   617  }
   618  
   619  // Ensure 1 DBHeader per Block Hash
   620  func uniqueHeaderPerBlockHash(allHeaders []*DBHeader) []*DBHeader {
   621  	uniqHeadersByHash := map[common.Hash]*DBHeader{}
   622  	for _, header := range allHeaders {
   623  		uniqHeader, ok := uniqHeadersByHash[header.Hash]
   624  		if ok {
   625  			if len(header.PreloadedTransactions) > 0 {
   626  				uniqHeader.PreloadedTransactions = append(uniqHeader.PreloadedTransactions, header.PreloadedTransactions...)
   627  			}
   628  			uniqHeadersByHash[header.Hash] = uniqHeader
   629  		} else {
   630  			uniqHeadersByHash[header.Hash] = header
   631  		}
   632  	}
   633  
   634  	uniqHeaders := []*DBHeader{}
   635  	for _, header := range uniqHeadersByHash {
   636  		uniqHeaders = append(uniqHeaders, header)
   637  	}
   638  
   639  	return uniqHeaders
   640  }
   641  
   642  // Organize subTransactions by Transaction Hash
   643  func subTransactionListToTransactionsByTxHash(subTransactions []Transfer) map[common.Hash]Transaction {
   644  	rst := map[common.Hash]Transaction{}
   645  
   646  	for index := range subTransactions {
   647  		subTx := &subTransactions[index]
   648  		txHash := subTx.Transaction.Hash()
   649  
   650  		if _, ok := rst[txHash]; !ok {
   651  			rst[txHash] = make([]*Transfer, 0)
   652  		}
   653  		rst[txHash] = append(rst[txHash], subTx)
   654  	}
   655  
   656  	return rst
   657  }
   658  
   659  func IsTransferDetectionEvent(ev walletevent.EventType) bool {
   660  	if ev == EventInternalETHTransferDetected ||
   661  		ev == EventInternalERC20TransferDetected ||
   662  		ev == EventInternalERC721TransferDetected ||
   663  		ev == EventInternalERC1155TransferDetected {
   664  		return true
   665  	}
   666  
   667  	return false
   668  }