status-go/services/stickers/api.go

544 lines
13 KiB
Go
Raw Normal View History

2022-02-02 22:50:55 +00:00
package stickers
import (
"context"
"database/sql"
"errors"
"io/ioutil"
"math/big"
"net/http"
"sync"
"time"
"github.com/ipfs/go-cid"
"github.com/multiformats/go-multibase"
"github.com/wealdtech/go-multicodec"
"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/multiaccounts/accounts"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/rpcfilters"
"github.com/status-im/status-go/services/wallet/bigint"
)
const ipfsGateway = ".ipfs.cf-ipfs.com"
// 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
rpcFiltersSrvc *rpcfilters.Service
config *params.NodeConfig
ctx context.Context
client *http.Client
}
type Sticker struct {
PackID *bigint.BigInt `json:"packID"`
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"`
Price *bigint.BigInt `json:"price"`
Preview string `json:"preview"`
Thumbnail string `json:"thumbnail"`
Stickers []Sticker `json:"stickers"`
Status stickerStatus `json:"status"`
}
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, appDB *sql.DB, rpcClient *rpc.Client, accountsManager *account.GethManager, rpcFiltersSrvc *rpcfilters.Service, config *params.NodeConfig) *API {
return &API{
contractMaker: &contracts.ContractMaker{
RPCClient: rpcClient,
},
accountsManager: accountsManager,
accountsDB: accounts.NewDB(appDB),
rpcFiltersSrvc: rpcFiltersSrvc,
config: config,
ctx: ctx,
client: &http.Client{
Timeout: time.Second * 5,
},
}
}
func (api *API) Market(chainID uint64) ([]StickerPack, error) {
// TODO: eventually this should be changed to include pagination
accounts, err := api.accountsDB.GetAccounts()
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, accounts, 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:
// TODO: add an attribute to indicate if the sticker pack
// is bought, but the transaction is still pending confirmation.
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)
}
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
}
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
wg := sync.WaitGroup{}
wg.Add(len(tokenIDs))
for _, tokenID := range tokenIDs {
go func(tokenID *big.Int) {
defer wg.Done()
packID, err := stickerPack.TokenPackId(callOpts, tokenID)
if err != nil {
errChan <- err
return
}
resultChan <- packID
}(tokenID)
}
wg.Wait()
}
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 hashToURL(hash []byte) (string, error) {
// contract response includes a contenthash, which needs to be decoded to reveal
// an IPFS identifier. Once decoded, download the content from IPFS. This content
// is in EDN format, ie https://ipfs.infura.io/ipfs/QmWVVLwVKCwkVNjYJrRzQWREVvEk917PhbHYAUhA1gECTM
// and it also needs to be decoded in to a nim type
data, codec, err := multicodec.RemoveCodec(hash)
if err != nil {
return "", err
}
codecName, err := multicodec.Name(codec)
if err != nil {
return "", err
}
if codecName != "ipfs-ns" {
return "", errors.New("codecName is not ipfs-ns")
}
thisCID, err := cid.Parse(data)
if err != nil {
return "", err
}
str, err := thisCID.StringOfBase(multibase.Base32)
if err != nil {
return "", err
}
return "https://" + str + ipfsGateway, nil
}
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
}
wg := sync.WaitGroup{}
wg.Add(int(numPacks.Int64()))
for i := uint64(0); i < numPacks.Uint64(); i++ {
go func(i uint64) {
defer wg.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 {
log.Warn("Could not retrieve stickerpack data", "packID", packID, "error", err)
return
}
resultChan <- stickerPack
}(i)
}
wg.Wait()
}
func (api *API) fetchPackData(stickerType *stickers.StickerType, packID *big.Int, translateHashes bool) (*StickerPack, error) {
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
packData, err := stickerType.GetPackData(callOpts, packID)
if err != nil {
return nil, err
}
packDetailsURL, err := hashToURL(packData.Contenthash)
if err != nil {
return nil, err
}
stickerPack := &StickerPack{
ID: &bigint.BigInt{Int: packID},
Owner: packData.Owner,
Price: &bigint.BigInt{Int: packData.Price},
}
err = api.downloadIPFSData(stickerPack, packDetailsURL, translateHashes)
if err != nil {
return nil, err
}
return stickerPack, nil
}
func (api *API) downloadIPFSData(stickerPack *StickerPack, packDetailsURL string, translateHashes bool) error {
// This can be improved by adding a cache using packDetailsURL as key
req, err := http.NewRequest(http.MethodGet, packDetailsURL, nil)
if err != nil {
return err
}
resp, err := api.client.Do(req)
if err != nil {
return err
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Error("failed to close the stickerpack request body", "err", err)
}
}()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return populateStickerPackAttributes(stickerPack, body, translateHashes)
}
func 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, err = decodeStringHash(stickerpackIPFSInfo.Meta.Preview)
if err != nil {
return err
}
stickerPack.Thumbnail, err = decodeStringHash(stickerpackIPFSInfo.Meta.Thumbnail)
if err != nil {
return err
}
} else {
stickerPack.Preview = stickerpackIPFSInfo.Meta.Preview
stickerPack.Thumbnail = stickerpackIPFSInfo.Meta.Thumbnail
}
for _, s := range stickerpackIPFSInfo.Meta.Stickers {
url := ""
if translateHashes {
hash, err := hexutil.Decode("0x" + s.Hash)
if err != nil {
return err
}
url, err = hashToURL(hash)
if err != nil {
return err
}
}
stickerPack.Stickers = append(stickerPack.Stickers, Sticker{
PackID: stickerPack.ID,
URL: url,
Hash: s.Hash,
})
}
return nil
}
func decodeStringHash(input string) (string, error) {
hash, err := hexutil.Decode("0x" + input)
if err != nil {
return "", err
}
url, err := hashToURL(hash)
if err != nil {
return "", err
}
return url, 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 close(doneChan)
defer close(errChan)
defer close(resultChan)
wg := sync.WaitGroup{}
wg.Add(len(accs))
for _, account := range accs {
go func(acc accounts.Account) {
defer wg.Done()
packs, err := api.getPurchasedPackIDs(chainID, acc.Address)
if err != nil {
errChan <- err
return
}
for _, p := range packs {
resultChan <- p
}
}(account)
}
wg.Wait()
}
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
}
callOpts := &bind.CallOpts{Context: api.ctx, Pending: false}
wg := sync.WaitGroup{}
wg.Add(int(balance.Int64()))
for i := uint64(0); i < balance.Uint64(); i++ {
go func(i uint64) {
defer wg.Done()
tokenID, err := stickerPack.TokenOfOwnerByIndex(callOpts, common.Address(account), new(big.Int).SetUint64(i))
if err != nil {
errChan <- err
return
}
resultChan <- tokenID
}(i)
}
wg.Wait()
}
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)
}
}
}
}