status-go/services/stickers/api.go

494 lines
12 KiB
Go

package stickers
import (
"context"
"math/big"
"time"
"github.com/zenthangplus/goccm"
"go.uber.org/zap"
"olympos.io/encoding/edn"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/status-im/status-go/account"
gocommon "github.com/status-im/status-go/common"
"github.com/status-im/status-go/contracts"
"github.com/status-im/status-go/contracts/stickers"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/ipfs"
"github.com/status-im/status-go/logutils"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/server"
"github.com/status-im/status-go/services/wallet/bigint"
"github.com/status-im/status-go/transactions"
)
const maxConcurrentRequests = 3
const requestTimeout = time.Duration(5) * time.Second
// ConnectionType constants
type stickerStatus int
const (
statusAvailable stickerStatus = iota
statusInstalled
statusPending
statusPurchased
)
type API struct {
contractMaker *contracts.ContractMaker
accountsManager *account.GethManager
accountsDB *accounts.Database
pendingTracker *transactions.PendingTxTracker
keyStoreDir string
downloader *ipfs.Downloader
httpServer *server.MediaServer
ctx context.Context
}
type Sticker struct {
PackID *bigint.BigInt `json:"packID,omitempty"`
URL string `json:"url,omitempty"`
Hash string `json:"hash,omitempty"`
}
type StickerPack struct {
ID *bigint.BigInt `json:"id"`
Name string `json:"name"`
Author string `json:"author"`
Owner common.Address `json:"owner,omitempty"`
Price *bigint.BigInt `json:"price"`
Preview string `json:"preview"`
Thumbnail string `json:"thumbnail"`
Stickers []Sticker `json:"stickers"`
Status stickerStatus `json:"status"`
}
type StickerPackCollection map[uint]StickerPack
type ednSticker struct {
Hash string
}
type ednStickerPack struct {
Name string
Author string
Thumbnail string
Preview string
Stickers []ednSticker
}
type ednStickerPackInfo struct {
Meta ednStickerPack
}
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 {
result := &API{
contractMaker: &contracts.ContractMaker{
RPCClient: rpcClient,
},
accountsManager: accountsManager,
accountsDB: acc,
pendingTracker: pendingTracker,
keyStoreDir: keyStoreDir,
downloader: downloader,
ctx: ctx,
httpServer: httpServer,
}
return result
}
func (api *API) Market(chainID uint64) ([]StickerPack, error) {
// TODO: eventually this should be changed to include pagination
accs, err := api.accountsDB.GetActiveAccounts()
if err != nil {
return nil, err
}
allStickerPacks, err := api.getContractPacks(chainID)
if err != nil {
return nil, err
}
purchasedPacks := make(map[uint]struct{})
purchasedPackChan := make(chan *big.Int)
errChan := make(chan error)
doneChan := make(chan struct{}, 1)
go api.getAccountsPurchasedPack(chainID, accs, purchasedPackChan, errChan, doneChan)
for {
select {
case err := <-errChan:
if err != nil {
return nil, err
}
case packID := <-purchasedPackChan:
if packID != nil {
purchasedPacks[uint(packID.Uint64())] = struct{}{}
}
case <-doneChan:
var result []StickerPack
for _, pack := range allStickerPacks {
packID := uint(pack.ID.Uint64())
_, isPurchased := purchasedPacks[packID]
if isPurchased {
pack.Status = statusPurchased
} else {
pack.Status = statusAvailable
}
result = append(result, pack)
}
return result, nil
}
}
}
func (api *API) execTokenPackID(chainID uint64, tokenIDs []*big.Int, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) {
defer gocommon.LogOnPanic()
defer close(doneChan)
defer close(errChan)
defer close(resultChan)
stickerPack, err := api.contractMaker.NewStickerPack(chainID)
if err != nil {
errChan <- err
return
}
if len(tokenIDs) == 0 {
return
}
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
c := goccm.New(maxConcurrentRequests)
for _, tokenID := range tokenIDs {
c.Wait()
go func(tokenID *big.Int) {
defer gocommon.LogOnPanic()
defer c.Done()
packID, err := stickerPack.TokenPackId(callOpts, tokenID)
if err != nil {
errChan <- err
return
}
resultChan <- packID
}(tokenID)
}
c.WaitAllDone()
}
func (api *API) getTokenPackIDs(chainID uint64, tokenIDs []*big.Int) ([]*big.Int, error) {
tokenPackIDChan := make(chan *big.Int)
errChan := make(chan error)
doneChan := make(chan struct{}, 1)
go api.execTokenPackID(chainID, tokenIDs, tokenPackIDChan, errChan, doneChan)
var tokenPackIDs []*big.Int
for {
select {
case <-doneChan:
return tokenPackIDs, nil
case err := <-errChan:
if err != nil {
return nil, err
}
case t := <-tokenPackIDChan:
if t != nil {
tokenPackIDs = append(tokenPackIDs, t)
}
}
}
}
func (api *API) getPurchasedPackIDs(chainID uint64, account types.Address) ([]*big.Int, error) {
// TODO: this should be replaced in the future by something like TheGraph to reduce the number of requests to infura
stickerPack, err := api.contractMaker.NewStickerPack(chainID)
if err != nil {
return nil, err
}
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
balance, err := stickerPack.BalanceOf(callOpts, common.Address(account))
if err != nil {
return nil, err
}
tokenIDs, err := api.getTokenOwnerOfIndex(chainID, account, balance)
if err != nil {
return nil, err
}
return api.getTokenPackIDs(chainID, tokenIDs)
}
func (api *API) fetchStickerPacks(chainID uint64, resultChan chan<- *StickerPack, errChan chan<- error, doneChan chan<- struct{}) {
defer gocommon.LogOnPanic()
defer close(doneChan)
defer close(errChan)
defer close(resultChan)
installedPacks, err := api.Installed()
if err != nil {
errChan <- err
return
}
pendingPacks, err := api.pendingStickerPacks()
if err != nil {
errChan <- err
return
}
stickerType, err := api.contractMaker.NewStickerType(chainID)
if err != nil {
errChan <- err
return
}
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
numPacks, err := stickerType.PackCount(callOpts)
if err != nil {
errChan <- err
return
}
if numPacks.Uint64() == 0 {
return
}
c := goccm.New(maxConcurrentRequests)
for i := uint64(0); i < numPacks.Uint64(); i++ {
c.Wait()
go func(i uint64) {
defer gocommon.LogOnPanic()
defer c.Done()
packID := new(big.Int).SetUint64(i)
_, exists := installedPacks[uint(i)]
if exists {
return // We already have the sticker pack data, no need to query it
}
_, exists = pendingPacks[uint(i)]
if exists {
return // We already have the sticker pack data, no need to query it
}
stickerPack, err := api.fetchPackData(stickerType, packID, true)
if err != nil {
logutils.ZapLogger().Warn("Could not retrieve stickerpack data",
zap.Uint64("packID", packID.Uint64()),
zap.Error(err))
errChan <- err
return
}
resultChan <- stickerPack
}(i)
}
c.WaitAllDone()
}
func (api *API) fetchPackData(stickerType *stickers.StickerType, packID *big.Int, translateHashes bool) (*StickerPack, error) {
timeoutContext, timeoutCancel := context.WithTimeout(api.ctx, requestTimeout)
defer timeoutCancel()
callOpts := &bind.CallOpts{Context: timeoutContext, Pending: false}
packData, err := stickerType.GetPackData(callOpts, packID)
if err != nil {
return nil, err
}
stickerPack := &StickerPack{
ID: &bigint.BigInt{Int: packID},
Owner: packData.Owner,
Price: &bigint.BigInt{Int: packData.Price},
}
err = api.downloadPackData(stickerPack, packData.Contenthash, translateHashes)
if err != nil {
return nil, err
}
return stickerPack, nil
}
func (api *API) downloadPackData(stickerPack *StickerPack, contentHash []byte, translateHashes bool) error {
fileContent, err := api.downloader.Get(hexutil.Encode(contentHash)[2:], true)
if err != nil {
return err
}
return api.populateStickerPackAttributes(stickerPack, fileContent, translateHashes)
}
func (api *API) hashToURL(hash string) string {
return api.httpServer.MakeStickerURL(hash)
}
func (api *API) populateStickerPackAttributes(stickerPack *StickerPack, ednSource []byte, translateHashes bool) error {
var stickerpackIPFSInfo ednStickerPackInfo
err := edn.Unmarshal(ednSource, &stickerpackIPFSInfo)
if err != nil {
return err
}
stickerPack.Author = stickerpackIPFSInfo.Meta.Author
stickerPack.Name = stickerpackIPFSInfo.Meta.Name
if translateHashes {
stickerPack.Preview = api.hashToURL(stickerpackIPFSInfo.Meta.Preview)
stickerPack.Thumbnail = api.hashToURL(stickerpackIPFSInfo.Meta.Thumbnail)
} else {
stickerPack.Preview = stickerpackIPFSInfo.Meta.Preview
stickerPack.Thumbnail = stickerpackIPFSInfo.Meta.Thumbnail
}
for _, s := range stickerpackIPFSInfo.Meta.Stickers {
url := ""
if translateHashes {
url = api.hashToURL(s.Hash)
}
stickerPack.Stickers = append(stickerPack.Stickers, Sticker{
PackID: stickerPack.ID,
URL: url,
Hash: s.Hash,
})
}
return nil
}
func (api *API) getContractPacks(chainID uint64) ([]StickerPack, error) {
stickerPackChan := make(chan *StickerPack)
errChan := make(chan error)
doneChan := make(chan struct{}, 1)
go api.fetchStickerPacks(chainID, stickerPackChan, errChan, doneChan)
var packs []StickerPack
for {
select {
case <-doneChan:
return packs, nil
case err := <-errChan:
if err != nil {
return nil, err
}
case pack := <-stickerPackChan:
if pack != nil {
packs = append(packs, *pack)
}
}
}
}
func (api *API) getAccountsPurchasedPack(chainID uint64, accs []*accounts.Account, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) {
defer gocommon.LogOnPanic()
defer close(doneChan)
defer close(errChan)
defer close(resultChan)
if len(accs) == 0 {
return
}
c := goccm.New(maxConcurrentRequests)
for _, account := range accs {
c.Wait()
go func(acc *accounts.Account) {
defer gocommon.LogOnPanic()
defer c.Done()
packs, err := api.getPurchasedPackIDs(chainID, acc.Address)
if err != nil {
errChan <- err
return
}
for _, p := range packs {
resultChan <- p
}
}(account)
}
c.WaitAllDone()
}
func (api *API) execTokenOwnerOfIndex(chainID uint64, account types.Address, balance *big.Int, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) {
defer gocommon.LogOnPanic()
defer close(doneChan)
defer close(errChan)
defer close(resultChan)
stickerPack, err := api.contractMaker.NewStickerPack(chainID)
if err != nil {
errChan <- err
return
}
if balance.Int64() == 0 {
return
}
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
c := goccm.New(maxConcurrentRequests)
for i := uint64(0); i < balance.Uint64(); i++ {
c.Wait()
go func(i uint64) {
defer gocommon.LogOnPanic()
defer c.Done()
tokenID, err := stickerPack.TokenOfOwnerByIndex(callOpts, common.Address(account), new(big.Int).SetUint64(i))
if err != nil {
errChan <- err
return
}
resultChan <- tokenID
}(i)
}
c.WaitAllDone()
}
func (api *API) getTokenOwnerOfIndex(chainID uint64, account types.Address, balance *big.Int) ([]*big.Int, error) {
tokenIDChan := make(chan *big.Int)
errChan := make(chan error)
doneChan := make(chan struct{}, 1)
go api.execTokenOwnerOfIndex(chainID, account, balance, tokenIDChan, errChan, doneChan)
var tokenIDs []*big.Int
for {
select {
case <-doneChan:
return tokenIDs, nil
case err := <-errChan:
if err != nil {
return nil, err
}
case tokenID := <-tokenIDChan:
if tokenID != nil {
tokenIDs = append(tokenIDs, tokenID)
}
}
}
}