github.com/status-im/status-go@v1.1.0/services/stickers/api.go (about)

     1  package stickers
     2  
     3  import (
     4  	"context"
     5  	"math/big"
     6  	"time"
     7  
     8  	"github.com/zenthangplus/goccm"
     9  	"olympos.io/encoding/edn"
    10  
    11  	"github.com/ethereum/go-ethereum/accounts/abi/bind"
    12  	"github.com/ethereum/go-ethereum/common"
    13  	"github.com/ethereum/go-ethereum/common/hexutil"
    14  	"github.com/ethereum/go-ethereum/log"
    15  	"github.com/status-im/status-go/account"
    16  	"github.com/status-im/status-go/contracts"
    17  	"github.com/status-im/status-go/contracts/stickers"
    18  	"github.com/status-im/status-go/eth-node/types"
    19  	"github.com/status-im/status-go/ipfs"
    20  	"github.com/status-im/status-go/multiaccounts/accounts"
    21  	"github.com/status-im/status-go/rpc"
    22  	"github.com/status-im/status-go/server"
    23  	"github.com/status-im/status-go/services/wallet/bigint"
    24  	"github.com/status-im/status-go/transactions"
    25  )
    26  
    27  const maxConcurrentRequests = 3
    28  const requestTimeout = time.Duration(5) * time.Second
    29  
    30  // ConnectionType constants
    31  type stickerStatus int
    32  
    33  const (
    34  	statusAvailable stickerStatus = iota
    35  	statusInstalled
    36  	statusPending
    37  	statusPurchased
    38  )
    39  
    40  type API struct {
    41  	contractMaker   *contracts.ContractMaker
    42  	accountsManager *account.GethManager
    43  	accountsDB      *accounts.Database
    44  	pendingTracker  *transactions.PendingTxTracker
    45  
    46  	keyStoreDir string
    47  	downloader  *ipfs.Downloader
    48  	httpServer  *server.MediaServer
    49  
    50  	ctx context.Context
    51  }
    52  
    53  type Sticker struct {
    54  	PackID *bigint.BigInt `json:"packID,omitempty"`
    55  	URL    string         `json:"url,omitempty"`
    56  	Hash   string         `json:"hash,omitempty"`
    57  }
    58  
    59  type StickerPack struct {
    60  	ID        *bigint.BigInt `json:"id"`
    61  	Name      string         `json:"name"`
    62  	Author    string         `json:"author"`
    63  	Owner     common.Address `json:"owner,omitempty"`
    64  	Price     *bigint.BigInt `json:"price"`
    65  	Preview   string         `json:"preview"`
    66  	Thumbnail string         `json:"thumbnail"`
    67  	Stickers  []Sticker      `json:"stickers"`
    68  
    69  	Status stickerStatus `json:"status"`
    70  }
    71  
    72  type StickerPackCollection map[uint]StickerPack
    73  
    74  type ednSticker struct {
    75  	Hash string
    76  }
    77  
    78  type ednStickerPack struct {
    79  	Name      string
    80  	Author    string
    81  	Thumbnail string
    82  	Preview   string
    83  	Stickers  []ednSticker
    84  }
    85  type ednStickerPackInfo struct {
    86  	Meta ednStickerPack
    87  }
    88  
    89  func NewAPI(ctx context.Context, acc *accounts.Database, rpcClient *rpc.Client, accountsManager *account.GethManager, pendingTracker *transactions.PendingTxTracker, keyStoreDir string, downloader *ipfs.Downloader, httpServer *server.MediaServer) *API {
    90  	result := &API{
    91  		contractMaker: &contracts.ContractMaker{
    92  			RPCClient: rpcClient,
    93  		},
    94  		accountsManager: accountsManager,
    95  		accountsDB:      acc,
    96  		pendingTracker:  pendingTracker,
    97  		keyStoreDir:     keyStoreDir,
    98  		downloader:      downloader,
    99  		ctx:             ctx,
   100  		httpServer:      httpServer,
   101  	}
   102  
   103  	return result
   104  }
   105  
   106  func (api *API) Market(chainID uint64) ([]StickerPack, error) {
   107  	// TODO: eventually this should be changed to include pagination
   108  	accs, err := api.accountsDB.GetActiveAccounts()
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	allStickerPacks, err := api.getContractPacks(chainID)
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	purchasedPacks := make(map[uint]struct{})
   119  
   120  	purchasedPackChan := make(chan *big.Int)
   121  	errChan := make(chan error)
   122  	doneChan := make(chan struct{}, 1)
   123  	go api.getAccountsPurchasedPack(chainID, accs, purchasedPackChan, errChan, doneChan)
   124  
   125  	for {
   126  		select {
   127  		case err := <-errChan:
   128  			if err != nil {
   129  				return nil, err
   130  			}
   131  		case packID := <-purchasedPackChan:
   132  			if packID != nil {
   133  				purchasedPacks[uint(packID.Uint64())] = struct{}{}
   134  			}
   135  
   136  		case <-doneChan:
   137  			var result []StickerPack
   138  			for _, pack := range allStickerPacks {
   139  				packID := uint(pack.ID.Uint64())
   140  				_, isPurchased := purchasedPacks[packID]
   141  				if isPurchased {
   142  					pack.Status = statusPurchased
   143  				} else {
   144  					pack.Status = statusAvailable
   145  				}
   146  				result = append(result, pack)
   147  			}
   148  
   149  			return result, nil
   150  		}
   151  	}
   152  }
   153  
   154  func (api *API) execTokenPackID(chainID uint64, tokenIDs []*big.Int, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) {
   155  	defer close(doneChan)
   156  	defer close(errChan)
   157  	defer close(resultChan)
   158  
   159  	stickerPack, err := api.contractMaker.NewStickerPack(chainID)
   160  	if err != nil {
   161  		errChan <- err
   162  		return
   163  	}
   164  
   165  	if len(tokenIDs) == 0 {
   166  		return
   167  	}
   168  
   169  	callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
   170  
   171  	c := goccm.New(maxConcurrentRequests)
   172  	for _, tokenID := range tokenIDs {
   173  		c.Wait()
   174  		go func(tokenID *big.Int) {
   175  			defer c.Done()
   176  			packID, err := stickerPack.TokenPackId(callOpts, tokenID)
   177  			if err != nil {
   178  				errChan <- err
   179  				return
   180  			}
   181  			resultChan <- packID
   182  		}(tokenID)
   183  	}
   184  	c.WaitAllDone()
   185  }
   186  
   187  func (api *API) getTokenPackIDs(chainID uint64, tokenIDs []*big.Int) ([]*big.Int, error) {
   188  	tokenPackIDChan := make(chan *big.Int)
   189  	errChan := make(chan error)
   190  	doneChan := make(chan struct{}, 1)
   191  
   192  	go api.execTokenPackID(chainID, tokenIDs, tokenPackIDChan, errChan, doneChan)
   193  
   194  	var tokenPackIDs []*big.Int
   195  	for {
   196  		select {
   197  		case <-doneChan:
   198  			return tokenPackIDs, nil
   199  		case err := <-errChan:
   200  			if err != nil {
   201  				return nil, err
   202  			}
   203  		case t := <-tokenPackIDChan:
   204  			if t != nil {
   205  				tokenPackIDs = append(tokenPackIDs, t)
   206  			}
   207  		}
   208  	}
   209  }
   210  
   211  func (api *API) getPurchasedPackIDs(chainID uint64, account types.Address) ([]*big.Int, error) {
   212  	// TODO: this should be replaced in the future by something like TheGraph to reduce the number of requests to infura
   213  
   214  	stickerPack, err := api.contractMaker.NewStickerPack(chainID)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  
   219  	callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
   220  
   221  	balance, err := stickerPack.BalanceOf(callOpts, common.Address(account))
   222  	if err != nil {
   223  		return nil, err
   224  	}
   225  
   226  	tokenIDs, err := api.getTokenOwnerOfIndex(chainID, account, balance)
   227  	if err != nil {
   228  		return nil, err
   229  	}
   230  
   231  	return api.getTokenPackIDs(chainID, tokenIDs)
   232  }
   233  
   234  func (api *API) fetchStickerPacks(chainID uint64, resultChan chan<- *StickerPack, errChan chan<- error, doneChan chan<- struct{}) {
   235  	defer close(doneChan)
   236  	defer close(errChan)
   237  	defer close(resultChan)
   238  
   239  	installedPacks, err := api.Installed()
   240  	if err != nil {
   241  		errChan <- err
   242  		return
   243  	}
   244  
   245  	pendingPacks, err := api.pendingStickerPacks()
   246  	if err != nil {
   247  		errChan <- err
   248  		return
   249  	}
   250  
   251  	stickerType, err := api.contractMaker.NewStickerType(chainID)
   252  	if err != nil {
   253  		errChan <- err
   254  		return
   255  	}
   256  
   257  	callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
   258  
   259  	numPacks, err := stickerType.PackCount(callOpts)
   260  	if err != nil {
   261  		errChan <- err
   262  		return
   263  	}
   264  
   265  	if numPacks.Uint64() == 0 {
   266  		return
   267  	}
   268  
   269  	c := goccm.New(maxConcurrentRequests)
   270  	for i := uint64(0); i < numPacks.Uint64(); i++ {
   271  		c.Wait()
   272  		go func(i uint64) {
   273  			defer c.Done()
   274  
   275  			packID := new(big.Int).SetUint64(i)
   276  
   277  			_, exists := installedPacks[uint(i)]
   278  			if exists {
   279  				return // We already have the sticker pack data, no need to query it
   280  			}
   281  
   282  			_, exists = pendingPacks[uint(i)]
   283  			if exists {
   284  				return // We already have the sticker pack data, no need to query it
   285  			}
   286  
   287  			stickerPack, err := api.fetchPackData(stickerType, packID, true)
   288  			if err != nil {
   289  				log.Warn("Could not retrieve stickerpack data", "packID", packID, "error", err)
   290  				errChan <- err
   291  				return
   292  			}
   293  
   294  			resultChan <- stickerPack
   295  		}(i)
   296  	}
   297  
   298  	c.WaitAllDone()
   299  }
   300  
   301  func (api *API) fetchPackData(stickerType *stickers.StickerType, packID *big.Int, translateHashes bool) (*StickerPack, error) {
   302  
   303  	timeoutContext, timeoutCancel := context.WithTimeout(api.ctx, requestTimeout)
   304  	defer timeoutCancel()
   305  
   306  	callOpts := &bind.CallOpts{Context: timeoutContext, Pending: false}
   307  	packData, err := stickerType.GetPackData(callOpts, packID)
   308  	if err != nil {
   309  		return nil, err
   310  	}
   311  
   312  	stickerPack := &StickerPack{
   313  		ID:    &bigint.BigInt{Int: packID},
   314  		Owner: packData.Owner,
   315  		Price: &bigint.BigInt{Int: packData.Price},
   316  	}
   317  
   318  	err = api.downloadPackData(stickerPack, packData.Contenthash, translateHashes)
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  
   323  	return stickerPack, nil
   324  }
   325  
   326  func (api *API) downloadPackData(stickerPack *StickerPack, contentHash []byte, translateHashes bool) error {
   327  	fileContent, err := api.downloader.Get(hexutil.Encode(contentHash)[2:], true)
   328  	if err != nil {
   329  		return err
   330  	}
   331  	return api.populateStickerPackAttributes(stickerPack, fileContent, translateHashes)
   332  }
   333  
   334  func (api *API) hashToURL(hash string) string {
   335  	return api.httpServer.MakeStickerURL(hash)
   336  }
   337  
   338  func (api *API) populateStickerPackAttributes(stickerPack *StickerPack, ednSource []byte, translateHashes bool) error {
   339  	var stickerpackIPFSInfo ednStickerPackInfo
   340  	err := edn.Unmarshal(ednSource, &stickerpackIPFSInfo)
   341  	if err != nil {
   342  		return err
   343  	}
   344  
   345  	stickerPack.Author = stickerpackIPFSInfo.Meta.Author
   346  	stickerPack.Name = stickerpackIPFSInfo.Meta.Name
   347  
   348  	if translateHashes {
   349  		stickerPack.Preview = api.hashToURL(stickerpackIPFSInfo.Meta.Preview)
   350  		stickerPack.Thumbnail = api.hashToURL(stickerpackIPFSInfo.Meta.Thumbnail)
   351  	} else {
   352  		stickerPack.Preview = stickerpackIPFSInfo.Meta.Preview
   353  		stickerPack.Thumbnail = stickerpackIPFSInfo.Meta.Thumbnail
   354  	}
   355  
   356  	for _, s := range stickerpackIPFSInfo.Meta.Stickers {
   357  		url := ""
   358  		if translateHashes {
   359  			url = api.hashToURL(s.Hash)
   360  		}
   361  
   362  		stickerPack.Stickers = append(stickerPack.Stickers, Sticker{
   363  			PackID: stickerPack.ID,
   364  			URL:    url,
   365  			Hash:   s.Hash,
   366  		})
   367  	}
   368  
   369  	return nil
   370  }
   371  
   372  func (api *API) getContractPacks(chainID uint64) ([]StickerPack, error) {
   373  	stickerPackChan := make(chan *StickerPack)
   374  	errChan := make(chan error)
   375  	doneChan := make(chan struct{}, 1)
   376  
   377  	go api.fetchStickerPacks(chainID, stickerPackChan, errChan, doneChan)
   378  
   379  	var packs []StickerPack
   380  
   381  	for {
   382  		select {
   383  		case <-doneChan:
   384  			return packs, nil
   385  		case err := <-errChan:
   386  			if err != nil {
   387  				return nil, err
   388  			}
   389  		case pack := <-stickerPackChan:
   390  			if pack != nil {
   391  				packs = append(packs, *pack)
   392  			}
   393  		}
   394  	}
   395  }
   396  
   397  func (api *API) getAccountsPurchasedPack(chainID uint64, accs []*accounts.Account, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) {
   398  	defer close(doneChan)
   399  	defer close(errChan)
   400  	defer close(resultChan)
   401  
   402  	if len(accs) == 0 {
   403  		return
   404  	}
   405  
   406  	c := goccm.New(maxConcurrentRequests)
   407  	for _, account := range accs {
   408  		c.Wait()
   409  		go func(acc *accounts.Account) {
   410  			defer c.Done()
   411  			packs, err := api.getPurchasedPackIDs(chainID, acc.Address)
   412  			if err != nil {
   413  				errChan <- err
   414  				return
   415  			}
   416  
   417  			for _, p := range packs {
   418  				resultChan <- p
   419  			}
   420  		}(account)
   421  	}
   422  	c.WaitAllDone()
   423  }
   424  
   425  func (api *API) execTokenOwnerOfIndex(chainID uint64, account types.Address, balance *big.Int, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) {
   426  	defer close(doneChan)
   427  	defer close(errChan)
   428  	defer close(resultChan)
   429  
   430  	stickerPack, err := api.contractMaker.NewStickerPack(chainID)
   431  	if err != nil {
   432  		errChan <- err
   433  		return
   434  	}
   435  
   436  	if balance.Int64() == 0 {
   437  		return
   438  	}
   439  
   440  	callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
   441  
   442  	c := goccm.New(maxConcurrentRequests)
   443  	for i := uint64(0); i < balance.Uint64(); i++ {
   444  		c.Wait()
   445  		go func(i uint64) {
   446  			defer c.Done()
   447  			tokenID, err := stickerPack.TokenOfOwnerByIndex(callOpts, common.Address(account), new(big.Int).SetUint64(i))
   448  			if err != nil {
   449  				errChan <- err
   450  				return
   451  			}
   452  
   453  			resultChan <- tokenID
   454  		}(i)
   455  	}
   456  	c.WaitAllDone()
   457  }
   458  
   459  func (api *API) getTokenOwnerOfIndex(chainID uint64, account types.Address, balance *big.Int) ([]*big.Int, error) {
   460  	tokenIDChan := make(chan *big.Int)
   461  	errChan := make(chan error)
   462  	doneChan := make(chan struct{}, 1)
   463  
   464  	go api.execTokenOwnerOfIndex(chainID, account, balance, tokenIDChan, errChan, doneChan)
   465  
   466  	var tokenIDs []*big.Int
   467  	for {
   468  		select {
   469  		case <-doneChan:
   470  			return tokenIDs, nil
   471  		case err := <-errChan:
   472  			if err != nil {
   473  				return nil, err
   474  			}
   475  		case tokenID := <-tokenIDChan:
   476  			if tokenID != nil {
   477  				tokenIDs = append(tokenIDs, tokenID)
   478  			}
   479  		}
   480  	}
   481  }