status-go/services/stickers/api.go

482 lines
12 KiB
Go
Raw Normal View History

2022-02-02 22:50:55 +00:00
package stickers
import (
"context"
"math/big"
"time"
2022-02-02 22:50:55 +00:00
"github.com/zenthangplus/goccm"
2022-02-02 22:50:55 +00:00
"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/ethereum/go-ethereum/log"
"github.com/status-im/status-go/account"
"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"
2022-02-02 22:50:55 +00:00
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/server"
2022-02-02 22:50:55 +00:00
"github.com/status-im/status-go/services/wallet/bigint"
"github.com/status-im/status-go/transactions"
2022-02-02 22:50:55 +00:00
)
const maxConcurrentRequests = 3
const requestTimeout = time.Duration(5) * time.Second
2022-02-02 22:50:55 +00:00
// ConnectionType constants
type stickerStatus int
const (
2022-02-07 13:28:22 +00:00
statusAvailable stickerStatus = iota
statusInstalled
2022-02-02 22:50:55 +00:00
statusPending
2022-02-07 13:28:22 +00:00
statusPurchased
2022-02-02 22:50:55 +00:00
)
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
2022-02-02 22:50:55 +00:00
}
type Sticker struct {
Sync Settings (#2478) * Sync Settings * Added valueHandlers and Database singleton Some issues remain, need a way to comparing incoming sql.DB to check if the connection is to a different file or not. Maybe make singleton instance per filename * Added functionality to check the sqlite filename * Refactor of Database.SaveSyncSettings to be used as a handler * Implemented inteface for setting sync protobuf factories * Refactored and completed adhoc send setting sync * Tidying up * Immutability refactor * Refactor settings into dedicated package * Breakout structs * Tidy up * Refactor of bulk settings sync * Bug fixes * Addressing feedback * Fix code dropped during rebase * Fix for db closed * Fix for node config related crashes * Provisional fix for type assertion - issue 2 * Adding robust type assertion checks * Partial fix for null literal db storage and json encoding * Fix for passively handling nil sql.DB, and checking if elem has len and if len is 0 * Added test for preferred name behaviour * Adding saved sync settings to MessengerResponse * Completed granular initial sync and clock from network on save * add Settings to isEmpty * Refactor of protobufs, partially done * Added syncSetting receiver handling, some bug fixes * Fix for sticker packs * Implement inactive flag on sync protobuf factory * Refactor of types and structs * Added SettingField.CanSync functionality * Addressing rebase artifact * Refactor of Setting SELECT queries * Refactor of string return queries * VERSION bump and migration index bump * Deactiveate Sync Settings * Deactiveated preferred_name and send_status_updates Co-authored-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
2022-03-23 18:47:00 +00:00
PackID *bigint.BigInt `json:"packID,omitempty"`
2022-02-02 22:50:55 +00:00
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"`
Sync Settings (#2478) * Sync Settings * Added valueHandlers and Database singleton Some issues remain, need a way to comparing incoming sql.DB to check if the connection is to a different file or not. Maybe make singleton instance per filename * Added functionality to check the sqlite filename * Refactor of Database.SaveSyncSettings to be used as a handler * Implemented inteface for setting sync protobuf factories * Refactored and completed adhoc send setting sync * Tidying up * Immutability refactor * Refactor settings into dedicated package * Breakout structs * Tidy up * Refactor of bulk settings sync * Bug fixes * Addressing feedback * Fix code dropped during rebase * Fix for db closed * Fix for node config related crashes * Provisional fix for type assertion - issue 2 * Adding robust type assertion checks * Partial fix for null literal db storage and json encoding * Fix for passively handling nil sql.DB, and checking if elem has len and if len is 0 * Added test for preferred name behaviour * Adding saved sync settings to MessengerResponse * Completed granular initial sync and clock from network on save * add Settings to isEmpty * Refactor of protobufs, partially done * Added syncSetting receiver handling, some bug fixes * Fix for sticker packs * Implement inactive flag on sync protobuf factory * Refactor of types and structs * Added SettingField.CanSync functionality * Addressing rebase artifact * Refactor of Setting SELECT queries * Refactor of string return queries * VERSION bump and migration index bump * Deactiveate Sync Settings * Deactiveated preferred_name and send_status_updates Co-authored-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
2022-03-23 18:47:00 +00:00
Owner common.Address `json:"owner,omitempty"`
2022-02-02 22:50:55 +00:00
Price *bigint.BigInt `json:"price"`
Preview string `json:"preview"`
Thumbnail string `json:"thumbnail"`
Stickers []Sticker `json:"stickers"`
Status stickerStatus `json:"status"`
2022-02-02 22:50:55 +00:00
}
Sync Settings (#2478) * Sync Settings * Added valueHandlers and Database singleton Some issues remain, need a way to comparing incoming sql.DB to check if the connection is to a different file or not. Maybe make singleton instance per filename * Added functionality to check the sqlite filename * Refactor of Database.SaveSyncSettings to be used as a handler * Implemented inteface for setting sync protobuf factories * Refactored and completed adhoc send setting sync * Tidying up * Immutability refactor * Refactor settings into dedicated package * Breakout structs * Tidy up * Refactor of bulk settings sync * Bug fixes * Addressing feedback * Fix code dropped during rebase * Fix for db closed * Fix for node config related crashes * Provisional fix for type assertion - issue 2 * Adding robust type assertion checks * Partial fix for null literal db storage and json encoding * Fix for passively handling nil sql.DB, and checking if elem has len and if len is 0 * Added test for preferred name behaviour * Adding saved sync settings to MessengerResponse * Completed granular initial sync and clock from network on save * add Settings to isEmpty * Refactor of protobufs, partially done * Added syncSetting receiver handling, some bug fixes * Fix for sticker packs * Implement inactive flag on sync protobuf factory * Refactor of types and structs * Added SettingField.CanSync functionality * Addressing rebase artifact * Refactor of Setting SELECT queries * Refactor of string return queries * VERSION bump and migration index bump * Deactiveate Sync Settings * Deactiveated preferred_name and send_status_updates Co-authored-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
2022-03-23 18:47:00 +00:00
type StickerPackCollection map[uint]StickerPack
2022-02-02 22:50:55 +00:00
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{
2022-02-02 22:50:55 +00:00
contractMaker: &contracts.ContractMaker{
RPCClient: rpcClient,
},
accountsManager: accountsManager,
Sync Settings (#2478) * Sync Settings * Added valueHandlers and Database singleton Some issues remain, need a way to comparing incoming sql.DB to check if the connection is to a different file or not. Maybe make singleton instance per filename * Added functionality to check the sqlite filename * Refactor of Database.SaveSyncSettings to be used as a handler * Implemented inteface for setting sync protobuf factories * Refactored and completed adhoc send setting sync * Tidying up * Immutability refactor * Refactor settings into dedicated package * Breakout structs * Tidy up * Refactor of bulk settings sync * Bug fixes * Addressing feedback * Fix code dropped during rebase * Fix for db closed * Fix for node config related crashes * Provisional fix for type assertion - issue 2 * Adding robust type assertion checks * Partial fix for null literal db storage and json encoding * Fix for passively handling nil sql.DB, and checking if elem has len and if len is 0 * Added test for preferred name behaviour * Adding saved sync settings to MessengerResponse * Completed granular initial sync and clock from network on save * add Settings to isEmpty * Refactor of protobufs, partially done * Added syncSetting receiver handling, some bug fixes * Fix for sticker packs * Implement inactive flag on sync protobuf factory * Refactor of types and structs * Added SettingField.CanSync functionality * Addressing rebase artifact * Refactor of Setting SELECT queries * Refactor of string return queries * VERSION bump and migration index bump * Deactiveate Sync Settings * Deactiveated preferred_name and send_status_updates Co-authored-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
2022-03-23 18:47:00 +00:00
accountsDB: acc,
pendingTracker: pendingTracker,
keyStoreDir: keyStoreDir,
downloader: downloader,
2022-02-02 22:50:55 +00:00
ctx: ctx,
httpServer: httpServer,
2022-02-02 22:50:55 +00:00
}
return result
2022-02-02 22:50:55 +00:00
}
func (api *API) Market(chainID uint64) ([]StickerPack, error) {
// TODO: eventually this should be changed to include pagination
accs, err := api.accountsDB.GetActiveAccounts()
2022-02-02 22:50:55 +00:00
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)
Sync Settings (#2478) * Sync Settings * Added valueHandlers and Database singleton Some issues remain, need a way to comparing incoming sql.DB to check if the connection is to a different file or not. Maybe make singleton instance per filename * Added functionality to check the sqlite filename * Refactor of Database.SaveSyncSettings to be used as a handler * Implemented inteface for setting sync protobuf factories * Refactored and completed adhoc send setting sync * Tidying up * Immutability refactor * Refactor settings into dedicated package * Breakout structs * Tidy up * Refactor of bulk settings sync * Bug fixes * Addressing feedback * Fix code dropped during rebase * Fix for db closed * Fix for node config related crashes * Provisional fix for type assertion - issue 2 * Adding robust type assertion checks * Partial fix for null literal db storage and json encoding * Fix for passively handling nil sql.DB, and checking if elem has len and if len is 0 * Added test for preferred name behaviour * Adding saved sync settings to MessengerResponse * Completed granular initial sync and clock from network on save * add Settings to isEmpty * Refactor of protobufs, partially done * Added syncSetting receiver handling, some bug fixes * Fix for sticker packs * Implement inactive flag on sync protobuf factory * Refactor of types and structs * Added SettingField.CanSync functionality * Addressing rebase artifact * Refactor of Setting SELECT queries * Refactor of string return queries * VERSION bump and migration index bump * Deactiveate Sync Settings * Deactiveated preferred_name and send_status_updates Co-authored-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
2022-03-23 18:47:00 +00:00
go api.getAccountsPurchasedPack(chainID, accs, purchasedPackChan, errChan, doneChan)
2022-02-02 22:50:55 +00:00
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
2022-02-07 13:28:22 +00:00
} else {
pack.Status = statusAvailable
2022-02-02 22:50:55 +00:00
}
result = append(result, pack)
}
2022-02-02 22:50:55 +00:00
return result, nil
}
}
}
func (api *API) execTokenPackID(chainID uint64, tokenIDs []*big.Int, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) {
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
}
2022-02-02 22:50:55 +00:00
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
c := goccm.New(maxConcurrentRequests)
2022-02-02 22:50:55 +00:00
for _, tokenID := range tokenIDs {
c.Wait()
2022-02-02 22:50:55 +00:00
go func(tokenID *big.Int) {
defer c.Done()
2022-02-02 22:50:55 +00:00
packID, err := stickerPack.TokenPackId(callOpts, tokenID)
if err != nil {
errChan <- err
return
}
resultChan <- packID
}(tokenID)
}
c.WaitAllDone()
2022-02-02 22:50:55 +00:00
}
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 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)
2022-02-02 22:50:55 +00:00
for i := uint64(0); i < numPacks.Uint64(); i++ {
c.Wait()
2022-02-02 22:50:55 +00:00
go func(i uint64) {
defer c.Done()
2022-02-02 22:50:55 +00:00
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 {
log.Warn("Could not retrieve stickerpack data", "packID", packID, "error", err)
errChan <- err
2022-02-02 22:50:55 +00:00
return
}
resultChan <- stickerPack
}(i)
}
c.WaitAllDone()
2022-02-02 22:50:55 +00:00
}
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}
2022-02-02 22:50:55 +00:00
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)
2022-02-02 22:50:55 +00:00
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)
2022-02-02 22:50:55 +00:00
if err != nil {
return err
}
return api.populateStickerPackAttributes(stickerPack, fileContent, translateHashes)
}
2022-02-02 22:50:55 +00:00
func (api *API) hashToURL(hash string) string {
return api.httpServer.MakeStickerURL(hash)
2022-02-02 22:50:55 +00:00
}
func (api *API) populateStickerPackAttributes(stickerPack *StickerPack, ednSource []byte, translateHashes bool) error {
2022-02-02 22:50:55 +00:00
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)
2022-02-02 22:50:55 +00:00
} 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)
2022-02-02 22:50:55 +00:00
}
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)
}
}
}
}
2022-05-18 10:42:51 +00:00
func (api *API) getAccountsPurchasedPack(chainID uint64, accs []*accounts.Account, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) {
2022-02-02 22:50:55 +00:00
defer close(doneChan)
defer close(errChan)
defer close(resultChan)
if len(accs) == 0 {
return
}
c := goccm.New(maxConcurrentRequests)
2022-02-02 22:50:55 +00:00
for _, account := range accs {
c.Wait()
2022-05-18 10:42:51 +00:00
go func(acc *accounts.Account) {
defer c.Done()
2022-02-02 22:50:55 +00:00
packs, err := api.getPurchasedPackIDs(chainID, acc.Address)
if err != nil {
errChan <- err
return
}
for _, p := range packs {
resultChan <- p
}
}(account)
}
c.WaitAllDone()
2022-02-02 22:50:55 +00:00
}
func (api *API) execTokenOwnerOfIndex(chainID uint64, account types.Address, balance *big.Int, resultChan chan<- *big.Int, errChan chan<- error, doneChan chan<- struct{}) {
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
}
2022-02-02 22:50:55 +00:00
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
c := goccm.New(maxConcurrentRequests)
2022-02-02 22:50:55 +00:00
for i := uint64(0); i < balance.Uint64(); i++ {
c.Wait()
2022-02-02 22:50:55 +00:00
go func(i uint64) {
defer c.Done()
2022-02-02 22:50:55 +00:00
tokenID, err := stickerPack.TokenOfOwnerByIndex(callOpts, common.Address(account), new(big.Int).SetUint64(i))
if err != nil {
errChan <- err
return
}
resultChan <- tokenID
}(i)
}
c.WaitAllDone()
2022-02-02 22:50:55 +00:00
}
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)
}
}
}
}