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

     1  package collectibles
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"errors"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/ethereum/go-ethereum/common"
    11  	"github.com/ethereum/go-ethereum/event"
    12  	"github.com/ethereum/go-ethereum/log"
    13  	"github.com/status-im/status-go/multiaccounts/accounts"
    14  	"github.com/status-im/status-go/multiaccounts/settings"
    15  	"github.com/status-im/status-go/rpc/network"
    16  	"github.com/status-im/status-go/services/accounts/accountsevent"
    17  	"github.com/status-im/status-go/services/accounts/settingsevent"
    18  	"github.com/status-im/status-go/services/wallet/async"
    19  	walletCommon "github.com/status-im/status-go/services/wallet/common"
    20  	"github.com/status-im/status-go/services/wallet/transfer"
    21  	"github.com/status-im/status-go/services/wallet/walletevent"
    22  )
    23  
    24  const (
    25  	activityRefetchMarginSeconds = 30 * 60 // Trigger a fetch if activity is detected this many seconds before the last fetch
    26  )
    27  
    28  type commandPerChainID = map[walletCommon.ChainID]*periodicRefreshOwnedCollectiblesCommand
    29  type commandPerAddressAndChainID = map[common.Address]commandPerChainID
    30  
    31  type timerPerChainID = map[walletCommon.ChainID]*time.Timer
    32  type timerPerAddressAndChainID = map[common.Address]timerPerChainID
    33  
    34  type Controller struct {
    35  	manager      *Manager
    36  	ownershipDB  *OwnershipDB
    37  	walletFeed   *event.Feed
    38  	accountsDB   *accounts.Database
    39  	accountsFeed *event.Feed
    40  	settingsFeed *event.Feed
    41  
    42  	networkManager *network.Manager
    43  	cancelFn       context.CancelFunc
    44  
    45  	commands            commandPerAddressAndChainID
    46  	timers              timerPerAddressAndChainID
    47  	group               *async.Group
    48  	accountsWatcher     *accountsevent.Watcher
    49  	walletEventsWatcher *walletevent.Watcher
    50  	settingsWatcher     *settingsevent.Watcher
    51  
    52  	ownedCollectiblesChangeCb OwnedCollectiblesChangeCb
    53  	collectiblesTransferCb    TransferCb
    54  
    55  	commandsLock sync.RWMutex
    56  }
    57  
    58  func NewController(
    59  	db *sql.DB,
    60  	walletFeed *event.Feed,
    61  	accountsDB *accounts.Database,
    62  	accountsFeed *event.Feed,
    63  	settingsFeed *event.Feed,
    64  	networkManager *network.Manager,
    65  	manager *Manager) *Controller {
    66  	return &Controller{
    67  		manager:        manager,
    68  		ownershipDB:    NewOwnershipDB(db),
    69  		walletFeed:     walletFeed,
    70  		accountsDB:     accountsDB,
    71  		accountsFeed:   accountsFeed,
    72  		settingsFeed:   settingsFeed,
    73  		networkManager: networkManager,
    74  		commands:       make(commandPerAddressAndChainID),
    75  		timers:         make(timerPerAddressAndChainID),
    76  	}
    77  }
    78  
    79  func (c *Controller) SetOwnedCollectiblesChangeCb(cb OwnedCollectiblesChangeCb) {
    80  	c.ownedCollectiblesChangeCb = cb
    81  }
    82  
    83  func (c *Controller) SetCollectiblesTransferCb(cb TransferCb) {
    84  	c.collectiblesTransferCb = cb
    85  }
    86  
    87  func (c *Controller) Start() {
    88  	// Setup periodical collectibles refresh
    89  	_ = c.startPeriodicalOwnershipFetch()
    90  
    91  	// Setup collectibles fetch when a new account gets added
    92  	c.startAccountsWatcher()
    93  
    94  	// Setup collectibles fetch when relevant activity is detected
    95  	c.startWalletEventsWatcher()
    96  
    97  	// Setup collectibles fetch when chain-related settings change
    98  	c.startSettingsWatcher()
    99  }
   100  
   101  func (c *Controller) Stop() {
   102  	c.stopSettingsWatcher()
   103  
   104  	c.stopWalletEventsWatcher()
   105  
   106  	c.stopAccountsWatcher()
   107  
   108  	c.stopPeriodicalOwnershipFetch()
   109  }
   110  
   111  func (c *Controller) RefetchOwnedCollectibles() {
   112  	c.stopPeriodicalOwnershipFetch()
   113  	c.manager.ResetConnectionStatus()
   114  	_ = c.startPeriodicalOwnershipFetch()
   115  }
   116  
   117  func (c *Controller) GetCommandState(chainID walletCommon.ChainID, address common.Address) OwnershipState {
   118  	c.commandsLock.RLock()
   119  	defer c.commandsLock.RUnlock()
   120  
   121  	state := OwnershipStateIdle
   122  	if c.commands[address] != nil && c.commands[address][chainID] != nil {
   123  		state = c.commands[address][chainID].GetState()
   124  	}
   125  
   126  	return state
   127  }
   128  
   129  func (c *Controller) isPeriodicalOwnershipFetchRunning() bool {
   130  	return c.group != nil
   131  }
   132  
   133  // Starts periodical fetching for the all wallet addresses and all chains
   134  func (c *Controller) startPeriodicalOwnershipFetch() error {
   135  	c.commandsLock.Lock()
   136  	defer c.commandsLock.Unlock()
   137  
   138  	if c.isPeriodicalOwnershipFetchRunning() {
   139  		return nil
   140  	}
   141  
   142  	ctx, cancel := context.WithCancel(context.Background())
   143  	c.cancelFn = cancel
   144  
   145  	c.group = async.NewGroup(ctx)
   146  
   147  	addresses, err := c.accountsDB.GetWalletAddresses()
   148  	if err != nil {
   149  		return err
   150  	}
   151  
   152  	for _, addr := range addresses {
   153  		err := c.startPeriodicalOwnershipFetchForAccount(common.Address(addr))
   154  		if err != nil {
   155  			log.Error("Error starting periodical collectibles fetch for accpunt", "address", addr, "error", err)
   156  			return err
   157  		}
   158  	}
   159  
   160  	return nil
   161  }
   162  
   163  func (c *Controller) stopPeriodicalOwnershipFetch() {
   164  	c.commandsLock.Lock()
   165  	defer c.commandsLock.Unlock()
   166  
   167  	if !c.isPeriodicalOwnershipFetchRunning() {
   168  		return
   169  	}
   170  
   171  	if c.cancelFn != nil {
   172  		c.cancelFn()
   173  		c.cancelFn = nil
   174  	}
   175  	if c.group != nil {
   176  		c.group.Stop()
   177  		c.group.Wait()
   178  		c.group = nil
   179  		c.commands = make(commandPerAddressAndChainID)
   180  	}
   181  }
   182  
   183  // Starts (or restarts) periodical fetching for the given account address for all chains
   184  func (c *Controller) startPeriodicalOwnershipFetchForAccount(address common.Address) error {
   185  	log.Debug("wallet.api.collectibles.Controller Start periodical fetching", "address", address)
   186  
   187  	networks, err := c.networkManager.Get(false)
   188  	if err != nil {
   189  		return err
   190  	}
   191  
   192  	areTestNetworksEnabled, err := c.accountsDB.GetTestNetworksEnabled()
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	for _, network := range networks {
   198  		if network.IsTest != areTestNetworksEnabled {
   199  			continue
   200  		}
   201  		chainID := walletCommon.ChainID(network.ChainID)
   202  
   203  		err := c.startPeriodicalOwnershipFetchForAccountAndChainID(address, chainID, false)
   204  		if err != nil {
   205  			return err
   206  		}
   207  	}
   208  
   209  	return nil
   210  }
   211  
   212  // Starts (or restarts) periodical fetching for the given account address for all chains
   213  func (c *Controller) startPeriodicalOwnershipFetchForAccountAndChainID(address common.Address, chainID walletCommon.ChainID, delayed bool) error {
   214  	log.Debug("wallet.api.collectibles.Controller Start periodical fetching", "address", address, "chainID", chainID, "delayed", delayed)
   215  
   216  	if !c.isPeriodicalOwnershipFetchRunning() {
   217  		return errors.New("periodical fetch not initialized")
   218  	}
   219  
   220  	err := c.stopPeriodicalOwnershipFetchForAccountAndChainID(address, chainID)
   221  	if err != nil {
   222  		return err
   223  	}
   224  
   225  	if _, ok := c.commands[address]; !ok {
   226  		c.commands[address] = make(commandPerChainID)
   227  	}
   228  
   229  	command := newPeriodicRefreshOwnedCollectiblesCommand(
   230  		c.manager,
   231  		c.ownershipDB,
   232  		c.walletFeed,
   233  		chainID,
   234  		address,
   235  		c.ownedCollectiblesChangeCb,
   236  	)
   237  
   238  	c.commands[address][chainID] = command
   239  	if delayed {
   240  		c.group.Add(command.DelayedCommand())
   241  	} else {
   242  		c.group.Add(command.Command())
   243  	}
   244  
   245  	return nil
   246  }
   247  
   248  // Stop periodical fetching for the given account address for all chains
   249  func (c *Controller) stopPeriodicalOwnershipFetchForAccount(address common.Address) error {
   250  	log.Debug("wallet.api.collectibles.Controller Stop periodical fetching", "address", address)
   251  
   252  	if !c.isPeriodicalOwnershipFetchRunning() {
   253  		return errors.New("periodical fetch not initialized")
   254  	}
   255  
   256  	if _, ok := c.commands[address]; ok {
   257  		for chainID := range c.commands[address] {
   258  			err := c.stopPeriodicalOwnershipFetchForAccountAndChainID(address, chainID)
   259  			if err != nil {
   260  				return err
   261  			}
   262  		}
   263  
   264  	}
   265  
   266  	return nil
   267  }
   268  
   269  func (c *Controller) stopPeriodicalOwnershipFetchForAccountAndChainID(address common.Address, chainID walletCommon.ChainID) error {
   270  	log.Debug("wallet.api.collectibles.Controller Stop periodical fetching", "address", address, "chainID", chainID)
   271  
   272  	if !c.isPeriodicalOwnershipFetchRunning() {
   273  		return errors.New("periodical fetch not initialized")
   274  	}
   275  
   276  	if _, ok := c.commands[address]; ok {
   277  		if _, ok := c.commands[address][chainID]; ok {
   278  			c.commands[address][chainID].Stop()
   279  			delete(c.commands[address], chainID)
   280  		}
   281  		// If it was the last chain, delete the address as well
   282  		if len(c.commands[address]) == 0 {
   283  			delete(c.commands, address)
   284  		}
   285  	}
   286  
   287  	return nil
   288  }
   289  
   290  func (c *Controller) startAccountsWatcher() {
   291  	if c.accountsWatcher != nil {
   292  		return
   293  	}
   294  
   295  	accountChangeCb := func(changedAddresses []common.Address, eventType accountsevent.EventType, currentAddresses []common.Address) {
   296  		c.commandsLock.Lock()
   297  		defer c.commandsLock.Unlock()
   298  		// Whenever an account gets added, start fetching
   299  		if eventType == accountsevent.EventTypeAdded {
   300  			for _, address := range changedAddresses {
   301  				err := c.startPeriodicalOwnershipFetchForAccount(address)
   302  				if err != nil {
   303  					log.Error("Error starting periodical collectibles fetch", "address", address, "error", err)
   304  				}
   305  			}
   306  		} else if eventType == accountsevent.EventTypeRemoved {
   307  			for _, address := range changedAddresses {
   308  				err := c.stopPeriodicalOwnershipFetchForAccount(address)
   309  				if err != nil {
   310  					log.Error("Error starting periodical collectibles fetch", "address", address, "error", err)
   311  				}
   312  			}
   313  		}
   314  	}
   315  
   316  	c.accountsWatcher = accountsevent.NewWatcher(c.accountsDB, c.accountsFeed, accountChangeCb)
   317  
   318  	c.accountsWatcher.Start()
   319  }
   320  
   321  func (c *Controller) stopAccountsWatcher() {
   322  	if c.accountsWatcher != nil {
   323  		c.accountsWatcher.Stop()
   324  		c.accountsWatcher = nil
   325  	}
   326  }
   327  
   328  func (c *Controller) startWalletEventsWatcher() {
   329  	if c.walletEventsWatcher != nil {
   330  		return
   331  	}
   332  
   333  	walletEventCb := func(event walletevent.Event) {
   334  		// EventRecentHistoryReady ?
   335  		if event.Type != transfer.EventInternalERC721TransferDetected &&
   336  			event.Type != transfer.EventInternalERC1155TransferDetected {
   337  			return
   338  		}
   339  
   340  		chainID := walletCommon.ChainID(event.ChainID)
   341  		for _, account := range event.Accounts {
   342  			// Call external callback
   343  			if c.collectiblesTransferCb != nil {
   344  				c.collectiblesTransferCb(account, chainID, event.EventParams.([]transfer.Transfer))
   345  			}
   346  
   347  			c.refetchOwnershipIfRecentTransfer(account, chainID, event.At)
   348  		}
   349  	}
   350  
   351  	c.walletEventsWatcher = walletevent.NewWatcher(c.walletFeed, walletEventCb)
   352  
   353  	c.walletEventsWatcher.Start()
   354  }
   355  
   356  func (c *Controller) stopWalletEventsWatcher() {
   357  	if c.walletEventsWatcher != nil {
   358  		c.walletEventsWatcher.Stop()
   359  		c.walletEventsWatcher = nil
   360  	}
   361  }
   362  
   363  func (c *Controller) startSettingsWatcher() {
   364  	if c.settingsWatcher != nil {
   365  		return
   366  	}
   367  
   368  	settingChangeCb := func(setting settings.SettingField, value interface{}) {
   369  		if setting.Equals(settings.TestNetworksEnabled) || setting.Equals(settings.IsGoerliEnabled) {
   370  			c.stopPeriodicalOwnershipFetch()
   371  			err := c.startPeriodicalOwnershipFetch()
   372  			if err != nil {
   373  				log.Error("Error starting periodical collectibles fetch", "error", err)
   374  			}
   375  		}
   376  	}
   377  
   378  	c.settingsWatcher = settingsevent.NewWatcher(c.settingsFeed, settingChangeCb)
   379  
   380  	c.settingsWatcher.Start()
   381  }
   382  
   383  func (c *Controller) stopSettingsWatcher() {
   384  	if c.settingsWatcher != nil {
   385  		c.settingsWatcher.Stop()
   386  		c.settingsWatcher = nil
   387  	}
   388  }
   389  
   390  func (c *Controller) refetchOwnershipIfRecentTransfer(account common.Address, chainID walletCommon.ChainID, latestTxTimestamp int64) {
   391  
   392  	// Check last ownership update timestamp
   393  	timestamp, err := c.ownershipDB.GetOwnershipUpdateTimestamp(account, chainID)
   394  
   395  	if err != nil {
   396  		log.Error("Error getting ownership update timestamp", "error", err)
   397  		return
   398  	}
   399  	if timestamp == InvalidTimestamp {
   400  		// Ownership was never fetched for this account
   401  		return
   402  	}
   403  
   404  	timeCheck := timestamp - activityRefetchMarginSeconds
   405  	if timeCheck < 0 {
   406  		timeCheck = 0
   407  	}
   408  
   409  	if latestTxTimestamp > timeCheck {
   410  		// Restart fetching for account + chainID
   411  		c.commandsLock.Lock()
   412  		err := c.startPeriodicalOwnershipFetchForAccountAndChainID(account, chainID, true)
   413  		c.commandsLock.Unlock()
   414  		if err != nil {
   415  			log.Error("Error starting periodical collectibles fetch", "address", account, "error", err)
   416  		}
   417  	}
   418  }