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

     1  package collectibles
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"math/big"
     8  	"sync/atomic"
     9  	"time"
    10  
    11  	"github.com/ethereum/go-ethereum/common"
    12  	"github.com/ethereum/go-ethereum/event"
    13  	"github.com/ethereum/go-ethereum/log"
    14  	"github.com/status-im/status-go/services/wallet/async"
    15  	"github.com/status-im/status-go/services/wallet/bigint"
    16  	walletCommon "github.com/status-im/status-go/services/wallet/common"
    17  	"github.com/status-im/status-go/services/wallet/thirdparty"
    18  	"github.com/status-im/status-go/services/wallet/transfer"
    19  	"github.com/status-im/status-go/services/wallet/walletevent"
    20  )
    21  
    22  const (
    23  	fetchLimit                          = 50 // Limit number of collectibles we fetch per provider call
    24  	accountOwnershipUpdateInterval      = 60 * time.Minute
    25  	accountOwnershipUpdateDelayInterval = 30 * time.Second
    26  )
    27  
    28  type OwnershipState = int
    29  
    30  type OwnedCollectibles struct {
    31  	chainID walletCommon.ChainID
    32  	account common.Address
    33  	ids     []thirdparty.CollectibleUniqueID
    34  }
    35  
    36  type OwnedCollectiblesChangeType = int
    37  
    38  const (
    39  	OwnedCollectiblesChangeTypeAdded OwnedCollectiblesChangeType = iota + 1
    40  	OwnedCollectiblesChangeTypeUpdated
    41  	OwnedCollectiblesChangeTypeRemoved
    42  )
    43  
    44  type OwnedCollectiblesChange struct {
    45  	ownedCollectibles OwnedCollectibles
    46  	changeType        OwnedCollectiblesChangeType
    47  }
    48  
    49  type OwnedCollectiblesChangeCb func(OwnedCollectiblesChange)
    50  
    51  type TransferCb func(common.Address, walletCommon.ChainID, []transfer.Transfer)
    52  
    53  const (
    54  	OwnershipStateIdle OwnershipState = iota + 1
    55  	OwnershipStateDelayed
    56  	OwnershipStateUpdating
    57  	OwnershipStateError
    58  )
    59  
    60  type periodicRefreshOwnedCollectiblesCommand struct {
    61  	chainID                   walletCommon.ChainID
    62  	account                   common.Address
    63  	manager                   *Manager
    64  	ownershipDB               *OwnershipDB
    65  	walletFeed                *event.Feed
    66  	ownedCollectiblesChangeCb OwnedCollectiblesChangeCb
    67  
    68  	group *async.Group
    69  	state atomic.Value
    70  }
    71  
    72  func newPeriodicRefreshOwnedCollectiblesCommand(
    73  	manager *Manager,
    74  	ownershipDB *OwnershipDB,
    75  	walletFeed *event.Feed,
    76  	chainID walletCommon.ChainID,
    77  	account common.Address,
    78  	ownedCollectiblesChangeCb OwnedCollectiblesChangeCb) *periodicRefreshOwnedCollectiblesCommand {
    79  	ret := &periodicRefreshOwnedCollectiblesCommand{
    80  		manager:                   manager,
    81  		ownershipDB:               ownershipDB,
    82  		walletFeed:                walletFeed,
    83  		chainID:                   chainID,
    84  		account:                   account,
    85  		ownedCollectiblesChangeCb: ownedCollectiblesChangeCb,
    86  	}
    87  	ret.state.Store(OwnershipStateIdle)
    88  	return ret
    89  }
    90  
    91  func (c *periodicRefreshOwnedCollectiblesCommand) DelayedCommand() async.Command {
    92  	return async.SingleShotCommand{
    93  		Interval: accountOwnershipUpdateDelayInterval,
    94  		Init: func(ctx context.Context) (err error) {
    95  			c.state.Store(OwnershipStateDelayed)
    96  			return nil
    97  		},
    98  		Runable: c.Command(),
    99  	}.Run
   100  }
   101  
   102  func (c *periodicRefreshOwnedCollectiblesCommand) Command() async.Command {
   103  	return async.InfiniteCommand{
   104  		Interval: accountOwnershipUpdateInterval,
   105  		Runable:  c.Run,
   106  	}.Run
   107  }
   108  
   109  func (c *periodicRefreshOwnedCollectiblesCommand) Run(ctx context.Context) (err error) {
   110  	return c.loadOwnedCollectibles(ctx)
   111  }
   112  
   113  func (c *periodicRefreshOwnedCollectiblesCommand) GetState() OwnershipState {
   114  	return c.state.Load().(OwnershipState)
   115  }
   116  
   117  func (c *periodicRefreshOwnedCollectiblesCommand) Stop() {
   118  	if c.group != nil {
   119  		c.group.Stop()
   120  		c.group.Wait()
   121  		c.group = nil
   122  	}
   123  }
   124  
   125  func (c *periodicRefreshOwnedCollectiblesCommand) loadOwnedCollectibles(ctx context.Context) error {
   126  	c.group = async.NewGroup(ctx)
   127  
   128  	ownedCollectiblesChangeCh := make(chan OwnedCollectiblesChange)
   129  	command := newLoadOwnedCollectiblesCommand(c.manager, c.ownershipDB, c.walletFeed, c.chainID, c.account, ownedCollectiblesChangeCh)
   130  
   131  	c.state.Store(OwnershipStateUpdating)
   132  	defer func() {
   133  		if command.err != nil {
   134  			c.state.Store(OwnershipStateError)
   135  		} else {
   136  			c.state.Store(OwnershipStateIdle)
   137  		}
   138  	}()
   139  
   140  	c.group.Add(command.Command())
   141  
   142  	select {
   143  	case ownedCollectiblesChange := <-ownedCollectiblesChangeCh:
   144  		if c.ownedCollectiblesChangeCb != nil {
   145  			c.ownedCollectiblesChangeCb(ownedCollectiblesChange)
   146  		}
   147  	case <-ctx.Done():
   148  		return ctx.Err()
   149  	case <-c.group.WaitAsync():
   150  		return nil
   151  	}
   152  
   153  	return nil
   154  }
   155  
   156  // Fetches owned collectibles for a ChainID+OwnerAddress combination in chunks
   157  // and updates the ownershipDB when all chunks are loaded
   158  type loadOwnedCollectiblesCommand struct {
   159  	chainID                   walletCommon.ChainID
   160  	account                   common.Address
   161  	manager                   *Manager
   162  	ownershipDB               *OwnershipDB
   163  	walletFeed                *event.Feed
   164  	ownedCollectiblesChangeCh chan<- OwnedCollectiblesChange
   165  
   166  	// Not to be set by the caller
   167  	partialOwnership []thirdparty.CollectibleIDBalance
   168  	err              error
   169  }
   170  
   171  func newLoadOwnedCollectiblesCommand(
   172  	manager *Manager,
   173  	ownershipDB *OwnershipDB,
   174  	walletFeed *event.Feed,
   175  	chainID walletCommon.ChainID,
   176  	account common.Address,
   177  	ownedCollectiblesChangeCh chan<- OwnedCollectiblesChange) *loadOwnedCollectiblesCommand {
   178  	return &loadOwnedCollectiblesCommand{
   179  		manager:                   manager,
   180  		ownershipDB:               ownershipDB,
   181  		walletFeed:                walletFeed,
   182  		chainID:                   chainID,
   183  		account:                   account,
   184  		ownedCollectiblesChangeCh: ownedCollectiblesChangeCh,
   185  	}
   186  }
   187  
   188  func (c *loadOwnedCollectiblesCommand) Command() async.Command {
   189  	return c.Run
   190  }
   191  
   192  func (c *loadOwnedCollectiblesCommand) triggerEvent(eventType walletevent.EventType, chainID walletCommon.ChainID, account common.Address, message string) {
   193  	c.walletFeed.Send(walletevent.Event{
   194  		Type:    eventType,
   195  		ChainID: uint64(chainID),
   196  		Accounts: []common.Address{
   197  			account,
   198  		},
   199  		Message: message,
   200  	})
   201  }
   202  
   203  func ownedTokensToTokenBalancesPerContractAddress(ownership []thirdparty.CollectibleIDBalance) thirdparty.TokenBalancesPerContractAddress {
   204  	ret := make(thirdparty.TokenBalancesPerContractAddress)
   205  	for _, idBalance := range ownership {
   206  		balanceBigInt := idBalance.Balance
   207  		if balanceBigInt == nil {
   208  			balanceBigInt = &bigint.BigInt{Int: big.NewInt(1)}
   209  		}
   210  		balance := thirdparty.TokenBalance{
   211  			TokenID: idBalance.ID.TokenID,
   212  			Balance: balanceBigInt,
   213  		}
   214  		ret[idBalance.ID.ContractID.Address] = append(ret[idBalance.ID.ContractID.Address], balance)
   215  	}
   216  	return ret
   217  }
   218  
   219  func (c *loadOwnedCollectiblesCommand) sendOwnedCollectiblesChanges(removed, updated, added []thirdparty.CollectibleUniqueID) {
   220  	if len(removed) > 0 {
   221  		c.ownedCollectiblesChangeCh <- OwnedCollectiblesChange{
   222  			ownedCollectibles: OwnedCollectibles{
   223  				chainID: c.chainID,
   224  				account: c.account,
   225  				ids:     removed,
   226  			},
   227  			changeType: OwnedCollectiblesChangeTypeRemoved,
   228  		}
   229  	}
   230  
   231  	if len(updated) > 0 {
   232  		c.ownedCollectiblesChangeCh <- OwnedCollectiblesChange{
   233  			ownedCollectibles: OwnedCollectibles{
   234  				chainID: c.chainID,
   235  				account: c.account,
   236  				ids:     updated,
   237  			},
   238  			changeType: OwnedCollectiblesChangeTypeUpdated,
   239  		}
   240  	}
   241  
   242  	if len(added) > 0 {
   243  		c.ownedCollectiblesChangeCh <- OwnedCollectiblesChange{
   244  			ownedCollectibles: OwnedCollectibles{
   245  				chainID: c.chainID,
   246  				account: c.account,
   247  				ids:     added,
   248  			},
   249  			changeType: OwnedCollectiblesChangeTypeAdded,
   250  		}
   251  	}
   252  }
   253  
   254  func (c *loadOwnedCollectiblesCommand) Run(parent context.Context) (err error) {
   255  	log.Debug("start loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account)
   256  
   257  	pageNr := 0
   258  	cursor := thirdparty.FetchFromStartCursor
   259  	providerID := thirdparty.FetchFromAnyProvider
   260  	start := time.Now()
   261  
   262  	c.triggerEvent(EventCollectiblesOwnershipUpdateStarted, c.chainID, c.account, "")
   263  
   264  	updateMessage := OwnershipUpdateMessage{}
   265  
   266  	lastFetchTimestamp, err := c.ownershipDB.GetOwnershipUpdateTimestamp(c.account, c.chainID)
   267  	if err != nil {
   268  		c.err = err
   269  	} else {
   270  		initialFetch := lastFetchTimestamp == InvalidTimestamp
   271  		// Fetch collectibles in chunks
   272  		for {
   273  			if walletCommon.ShouldCancel(parent) {
   274  				c.err = errors.New("context cancelled")
   275  				break
   276  			}
   277  
   278  			pageStart := time.Now()
   279  			log.Debug("start loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "page", pageNr)
   280  
   281  			partialOwnership, err := c.manager.FetchCollectibleOwnershipByOwner(parent, c.chainID, c.account, cursor, fetchLimit, providerID)
   282  
   283  			if err != nil {
   284  				log.Error("failed loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "page", pageNr, "error", err)
   285  				c.err = err
   286  				break
   287  			}
   288  
   289  			log.Debug("partial loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "page", pageNr, "in", time.Since(pageStart), "found", len(partialOwnership.Items))
   290  
   291  			c.partialOwnership = append(c.partialOwnership, partialOwnership.Items...)
   292  
   293  			pageNr++
   294  			cursor = partialOwnership.NextCursor
   295  			providerID = partialOwnership.Provider
   296  
   297  			finished := cursor == thirdparty.FetchFromStartCursor
   298  
   299  			// Normally, update the DB once we've finished fetching
   300  			// If this is the first fetch, make partial updates to the client to get a better UX
   301  			if initialFetch || finished {
   302  				balances := ownedTokensToTokenBalancesPerContractAddress(c.partialOwnership)
   303  
   304  				updateMessage.Removed, updateMessage.Updated, updateMessage.Added, err = c.ownershipDB.Update(c.chainID, c.account, balances, start.Unix())
   305  				if err != nil {
   306  					log.Error("failed updating ownershipDB in loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "error", err)
   307  					c.err = err
   308  					break
   309  				}
   310  
   311  				c.sendOwnedCollectiblesChanges(updateMessage.Removed, updateMessage.Updated, updateMessage.Added)
   312  			}
   313  
   314  			if finished || c.err != nil {
   315  				break
   316  			} else if initialFetch {
   317  				encodedMessage, err := json.Marshal(updateMessage)
   318  				if err != nil {
   319  					c.err = err
   320  					break
   321  				}
   322  				c.triggerEvent(EventCollectiblesOwnershipUpdatePartial, c.chainID, c.account, string(encodedMessage))
   323  
   324  				updateMessage = OwnershipUpdateMessage{}
   325  			}
   326  		}
   327  	}
   328  
   329  	var encodedMessage []byte
   330  	if c.err == nil {
   331  		encodedMessage, c.err = json.Marshal(updateMessage)
   332  	}
   333  
   334  	if c.err != nil {
   335  		c.triggerEvent(EventCollectiblesOwnershipUpdateFinishedWithError, c.chainID, c.account, c.err.Error())
   336  	} else {
   337  		c.triggerEvent(EventCollectiblesOwnershipUpdateFinished, c.chainID, c.account, string(encodedMessage))
   338  	}
   339  
   340  	log.Debug("end loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "in", time.Since(start))
   341  	return nil
   342  }