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

     1  package transfer
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"fmt"
     7  	"math/big"
     8  
     9  	"golang.org/x/exp/slices" // since 1.21, this is in the standard library
    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  	statusaccounts "github.com/status-im/status-go/multiaccounts/accounts"
    16  	"github.com/status-im/status-go/rpc"
    17  	"github.com/status-im/status-go/rpc/chain"
    18  	"github.com/status-im/status-go/services/accounts/accountsevent"
    19  	"github.com/status-im/status-go/services/wallet/balance"
    20  	"github.com/status-im/status-go/services/wallet/blockchainstate"
    21  	"github.com/status-im/status-go/services/wallet/token"
    22  	"github.com/status-im/status-go/transactions"
    23  )
    24  
    25  type Controller struct {
    26  	db                 *Database
    27  	accountsDB         *statusaccounts.Database
    28  	rpcClient          *rpc.Client
    29  	blockDAO           *BlockDAO
    30  	blockRangesSeqDAO  *BlockRangeSequentialDAO
    31  	reactor            *Reactor
    32  	accountFeed        *event.Feed
    33  	TransferFeed       *event.Feed
    34  	accWatcher         *accountsevent.Watcher
    35  	transactionManager *TransactionManager
    36  	pendingTxManager   *transactions.PendingTxTracker
    37  	tokenManager       *token.Manager
    38  	balanceCacher      balance.Cacher
    39  	blockChainState    *blockchainstate.BlockChainState
    40  }
    41  
    42  func NewTransferController(db *sql.DB, accountsDB *statusaccounts.Database, rpcClient *rpc.Client, accountFeed *event.Feed, transferFeed *event.Feed,
    43  	transactionManager *TransactionManager, pendingTxManager *transactions.PendingTxTracker, tokenManager *token.Manager,
    44  	balanceCacher balance.Cacher, blockChainState *blockchainstate.BlockChainState) *Controller {
    45  
    46  	blockDAO := &BlockDAO{db}
    47  	return &Controller{
    48  		db:                 NewDB(db),
    49  		accountsDB:         accountsDB,
    50  		blockDAO:           blockDAO,
    51  		blockRangesSeqDAO:  &BlockRangeSequentialDAO{db},
    52  		rpcClient:          rpcClient,
    53  		accountFeed:        accountFeed,
    54  		TransferFeed:       transferFeed,
    55  		transactionManager: transactionManager,
    56  		pendingTxManager:   pendingTxManager,
    57  		tokenManager:       tokenManager,
    58  		balanceCacher:      balanceCacher,
    59  		blockChainState:    blockChainState,
    60  	}
    61  }
    62  
    63  func (c *Controller) Start() {
    64  	go func() { _ = c.cleanupAccountsLeftovers() }()
    65  }
    66  
    67  func (c *Controller) Stop() {
    68  	if c.reactor != nil {
    69  		c.reactor.stop()
    70  	}
    71  
    72  	if c.accWatcher != nil {
    73  		c.accWatcher.Stop()
    74  		c.accWatcher = nil
    75  	}
    76  }
    77  
    78  func sameChains(chainIDs1 []uint64, chainIDs2 []uint64) bool {
    79  	if len(chainIDs1) != len(chainIDs2) {
    80  		return false
    81  	}
    82  
    83  	for _, chainID := range chainIDs1 {
    84  		if !slices.Contains(chainIDs2, chainID) {
    85  			return false
    86  		}
    87  	}
    88  
    89  	return true
    90  }
    91  
    92  func (c *Controller) CheckRecentHistory(chainIDs []uint64, accounts []common.Address) error {
    93  	if len(accounts) == 0 {
    94  		return nil
    95  	}
    96  
    97  	if len(chainIDs) == 0 {
    98  		return nil
    99  	}
   100  
   101  	err := c.blockDAO.mergeBlocksRanges(chainIDs, accounts)
   102  	if err != nil {
   103  		return err
   104  	}
   105  
   106  	chainClients, err := c.rpcClient.EthClients(chainIDs)
   107  	if err != nil {
   108  		return err
   109  	}
   110  
   111  	if c.reactor != nil {
   112  		if !sameChains(chainIDs, c.reactor.chainIDs) {
   113  			err := c.reactor.restart(chainClients, accounts)
   114  			if err != nil {
   115  				return err
   116  			}
   117  		}
   118  
   119  		return nil
   120  	}
   121  
   122  	const omitHistory = true
   123  	c.reactor = NewReactor(c.db, c.blockDAO, c.blockRangesSeqDAO, c.accountsDB, c.TransferFeed, c.transactionManager,
   124  		c.pendingTxManager, c.tokenManager, c.balanceCacher, omitHistory, c.blockChainState)
   125  
   126  	err = c.reactor.start(chainClients, accounts)
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	c.startAccountWatcher(chainIDs)
   132  
   133  	return nil
   134  }
   135  
   136  func (c *Controller) startAccountWatcher(chainIDs []uint64) {
   137  	if c.accWatcher == nil {
   138  		c.accWatcher = accountsevent.NewWatcher(c.accountsDB, c.accountFeed, func(changedAddresses []common.Address, eventType accountsevent.EventType, currentAddresses []common.Address) {
   139  			c.onAccountsChanged(changedAddresses, eventType, currentAddresses, chainIDs)
   140  		})
   141  	}
   142  	c.accWatcher.Start()
   143  }
   144  
   145  func (c *Controller) onAccountsChanged(changedAddresses []common.Address, eventType accountsevent.EventType, currentAddresses []common.Address, chainIDs []uint64) {
   146  	if eventType == accountsevent.EventTypeRemoved {
   147  		for _, address := range changedAddresses {
   148  			c.cleanUpRemovedAccount(address)
   149  		}
   150  	}
   151  
   152  	if c.reactor == nil {
   153  		log.Warn("reactor is not initialized")
   154  		return
   155  	}
   156  
   157  	if eventType == accountsevent.EventTypeAdded || eventType == accountsevent.EventTypeRemoved {
   158  		log.Debug("list of accounts was changed from a previous version. reactor will be restarted", "new", currentAddresses)
   159  
   160  		chainClients, err := c.rpcClient.EthClients(chainIDs)
   161  		if err != nil {
   162  			return
   163  		}
   164  
   165  		err = c.reactor.restart(chainClients, currentAddresses)
   166  		if err != nil {
   167  			log.Error("failed to restart reactor with new accounts", "error", err)
   168  		}
   169  	}
   170  }
   171  
   172  // Only used by status-mobile
   173  func (c *Controller) LoadTransferByHash(ctx context.Context, rpcClient *rpc.Client, address common.Address, hash common.Hash) error {
   174  	chainClient, err := rpcClient.EthClient(rpcClient.UpstreamChainID)
   175  	if err != nil {
   176  		return err
   177  	}
   178  
   179  	signer := types.LatestSignerForChainID(chainClient.ToBigInt())
   180  
   181  	transfer, err := getTransferByHash(ctx, chainClient, signer, address, hash)
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	transfers := []Transfer{*transfer}
   187  
   188  	err = c.db.InsertBlock(rpcClient.UpstreamChainID, address, transfer.BlockNumber, transfer.BlockHash)
   189  	if err != nil {
   190  		return err
   191  	}
   192  
   193  	tx, err := c.db.client.BeginTx(ctx, nil)
   194  	if err != nil {
   195  		return err
   196  	}
   197  
   198  	blocks := []*big.Int{transfer.BlockNumber}
   199  	err = saveTransfersMarkBlocksLoaded(tx, rpcClient.UpstreamChainID, address, transfers, blocks)
   200  	if err != nil {
   201  		rollErr := tx.Rollback()
   202  		if rollErr != nil {
   203  			return fmt.Errorf("failed to rollback transaction due to error: %v", err)
   204  		}
   205  		return err
   206  	}
   207  
   208  	return nil
   209  }
   210  
   211  func (c *Controller) GetTransfersByAddress(ctx context.Context, chainID uint64, address common.Address, toBlock *big.Int,
   212  	limit int64, fetchMore bool) ([]View, error) {
   213  
   214  	rst, err := c.reactor.getTransfersByAddress(ctx, chainID, address, toBlock, limit)
   215  	if err != nil {
   216  		log.Error("[WalletAPI:: GetTransfersByAddress] can't fetch transfers", "err", err)
   217  		return nil, err
   218  	}
   219  
   220  	return castToTransferViews(rst), nil
   221  }
   222  
   223  func (c *Controller) GetTransfersForIdentities(ctx context.Context, identities []TransactionIdentity) ([]View, error) {
   224  	rst, err := c.db.GetTransfersForIdentities(ctx, identities)
   225  	if err != nil {
   226  		log.Error("[transfer.Controller.GetTransfersForIdentities] DB err", err)
   227  		return nil, err
   228  	}
   229  
   230  	return castToTransferViews(rst), nil
   231  }
   232  
   233  func (c *Controller) GetCachedBalances(ctx context.Context, chainID uint64, addresses []common.Address) ([]BlockView, error) {
   234  	result, error := c.blockDAO.getLastKnownBlocks(chainID, addresses)
   235  	if error != nil {
   236  		return nil, error
   237  	}
   238  
   239  	return blocksToViews(result), nil
   240  }
   241  
   242  func (c *Controller) cleanUpRemovedAccount(address common.Address) {
   243  	// Transfers will be deleted by foreign key constraint by cascade
   244  	err := deleteBlocks(c.db.client, address)
   245  	if err != nil {
   246  		log.Error("Failed to delete blocks", "error", err)
   247  	}
   248  	err = deleteAllRanges(c.db.client, address)
   249  	if err != nil {
   250  		log.Error("Failed to delete old blocks ranges", "error", err)
   251  	}
   252  
   253  	err = c.blockRangesSeqDAO.deleteRange(address)
   254  	if err != nil {
   255  		log.Error("Failed to delete blocks ranges sequential", "error", err)
   256  	}
   257  
   258  	err = c.transactionManager.removeMultiTransactionByAddress(address)
   259  	if err != nil {
   260  		log.Error("Failed to delete multitransactions", "error", err)
   261  	}
   262  
   263  	rpcLimitsStorage := chain.NewLimitsDBStorage(c.db.client)
   264  	err = rpcLimitsStorage.Delete(accountLimiterTag(address))
   265  	if err != nil {
   266  		log.Error("Failed to delete limits", "error", err)
   267  	}
   268  }
   269  
   270  func (c *Controller) cleanupAccountsLeftovers() error {
   271  	// We clean up accounts that were deleted and soft removed
   272  	accounts, err := c.accountsDB.GetWalletAddresses()
   273  	if err != nil {
   274  		log.Error("Failed to get accounts", "error", err)
   275  		return err
   276  	}
   277  
   278  	existingAddresses := make([]common.Address, len(accounts))
   279  	for i, account := range accounts {
   280  		existingAddresses[i] = (common.Address)(account)
   281  	}
   282  
   283  	addressesInWalletDB, err := getAddresses(c.db.client)
   284  	if err != nil {
   285  		log.Error("Failed to get addresses from wallet db", "error", err)
   286  		return err
   287  	}
   288  
   289  	missing := findMissingItems(addressesInWalletDB, existingAddresses)
   290  	for _, address := range missing {
   291  		c.cleanUpRemovedAccount(address)
   292  	}
   293  
   294  	return nil
   295  }
   296  
   297  // find items from one slice that are not in another
   298  func findMissingItems(slice1 []common.Address, slice2 []common.Address) []common.Address {
   299  	var missing []common.Address
   300  	for _, item := range slice1 {
   301  		if !slices.Contains(slice2, item) {
   302  			missing = append(missing, item)
   303  		}
   304  	}
   305  	return missing
   306  }