Generated
Vendored
+339
@@ -0,0 +1,339 @@
|
||||
package collectibles
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/status-im/status-go/protocol/communities/token"
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
"github.com/status-im/status-go/sqlite"
|
||||
)
|
||||
|
||||
type CollectibleDataDB struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewCollectibleDataDB(sqlDb *sql.DB) *CollectibleDataDB {
|
||||
return &CollectibleDataDB{
|
||||
db: sqlDb,
|
||||
}
|
||||
}
|
||||
|
||||
const collectibleDataColumns = "chain_id, contract_address, token_id, provider, name, description, permalink, image_url, image_payload, animation_url, animation_media_type, background_color, token_uri, community_id"
|
||||
const collectibleCommunityDataColumns = "community_privileges_level"
|
||||
const collectibleTraitsColumns = "chain_id, contract_address, token_id, trait_type, trait_value, display_type, max_value"
|
||||
const selectCollectibleTraitsColumns = "trait_type, trait_value, display_type, max_value"
|
||||
|
||||
func rowsToCollectibleTraits(rows *sql.Rows) ([]thirdparty.CollectibleTrait, error) {
|
||||
var traits []thirdparty.CollectibleTrait = make([]thirdparty.CollectibleTrait, 0)
|
||||
for rows.Next() {
|
||||
var trait thirdparty.CollectibleTrait
|
||||
err := rows.Scan(
|
||||
&trait.TraitType,
|
||||
&trait.Value,
|
||||
&trait.DisplayType,
|
||||
&trait.MaxValue,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
traits = append(traits, trait)
|
||||
}
|
||||
return traits, nil
|
||||
}
|
||||
|
||||
func getCollectibleTraits(creator sqlite.StatementCreator, id thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleTrait, error) {
|
||||
// Get traits list
|
||||
selectTraits, err := creator.Prepare(fmt.Sprintf(`SELECT %s
|
||||
FROM collectible_traits_cache
|
||||
WHERE chain_id = ? AND contract_address = ? AND token_id = ?`, selectCollectibleTraitsColumns))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := selectTraits.Query(
|
||||
id.ContractID.ChainID,
|
||||
id.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(id.TokenID.Int),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rowsToCollectibleTraits(rows)
|
||||
}
|
||||
|
||||
func upsertCollectibleTraits(creator sqlite.StatementCreator, id thirdparty.CollectibleUniqueID, traits []thirdparty.CollectibleTrait) error {
|
||||
// Remove old traits list
|
||||
deleteTraits, err := creator.Prepare(`DELETE FROM collectible_traits_cache WHERE chain_id = ? AND contract_address = ? AND token_id = ?`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = deleteTraits.Exec(
|
||||
id.ContractID.ChainID,
|
||||
id.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(id.TokenID.Int),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert new traits list
|
||||
insertTrait, err := creator.Prepare(fmt.Sprintf(`INSERT INTO collectible_traits_cache (%s)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`, collectibleTraitsColumns))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, t := range traits {
|
||||
_, err = insertTrait.Exec(
|
||||
id.ContractID.ChainID,
|
||||
id.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(id.TokenID.Int),
|
||||
t.TraitType,
|
||||
t.Value,
|
||||
t.DisplayType,
|
||||
t.MaxValue,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setCollectiblesData(creator sqlite.StatementCreator, collectibles []thirdparty.CollectibleData, allowUpdate bool) error {
|
||||
insertCollectible, err := creator.Prepare(fmt.Sprintf(`%s INTO collectible_data_cache (%s)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, insertStatement(allowUpdate), collectibleDataColumns))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, c := range collectibles {
|
||||
_, err = insertCollectible.Exec(
|
||||
c.ID.ContractID.ChainID,
|
||||
c.ID.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(c.ID.TokenID.Int),
|
||||
c.Provider,
|
||||
c.Name,
|
||||
c.Description,
|
||||
c.Permalink,
|
||||
c.ImageURL,
|
||||
c.ImagePayload,
|
||||
c.AnimationURL,
|
||||
c.AnimationMediaType,
|
||||
c.BackgroundColor,
|
||||
c.TokenURI,
|
||||
c.CommunityID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = upsertContractType(creator, c.ID.ContractID, c.ContractType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if allowUpdate {
|
||||
err = upsertCollectibleTraits(creator, c.ID, c.Traits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *CollectibleDataDB) SetData(collectibles []thirdparty.CollectibleData, allowUpdate bool) (err error) {
|
||||
tx, err := o.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err == nil {
|
||||
err = tx.Commit()
|
||||
return
|
||||
}
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
// Insert new collectibles data
|
||||
err = setCollectiblesData(tx, collectibles, allowUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func scanCollectiblesDataRow(row *sql.Row) (*thirdparty.CollectibleData, error) {
|
||||
c := thirdparty.CollectibleData{
|
||||
ID: thirdparty.CollectibleUniqueID{
|
||||
TokenID: &bigint.BigInt{Int: big.NewInt(0)},
|
||||
},
|
||||
Traits: make([]thirdparty.CollectibleTrait, 0),
|
||||
}
|
||||
err := row.Scan(
|
||||
&c.ID.ContractID.ChainID,
|
||||
&c.ID.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(c.ID.TokenID.Int),
|
||||
&c.Provider,
|
||||
&c.Name,
|
||||
&c.Description,
|
||||
&c.Permalink,
|
||||
&c.ImageURL,
|
||||
&c.ImagePayload,
|
||||
&c.AnimationURL,
|
||||
&c.AnimationMediaType,
|
||||
&c.BackgroundColor,
|
||||
&c.TokenURI,
|
||||
&c.CommunityID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (o *CollectibleDataDB) GetIDsNotInDB(ids []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleUniqueID, error) {
|
||||
ret := make([]thirdparty.CollectibleUniqueID, 0, len(ids))
|
||||
|
||||
exists, err := o.db.Prepare(`SELECT EXISTS (
|
||||
SELECT 1 FROM collectible_data_cache
|
||||
WHERE chain_id=? AND contract_address=? AND token_id=?
|
||||
)`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
row := exists.QueryRow(
|
||||
id.ContractID.ChainID,
|
||||
id.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(id.TokenID.Int),
|
||||
)
|
||||
var exists bool
|
||||
err = row.Scan(&exists)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
ret = append(ret, id)
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (o *CollectibleDataDB) GetData(ids []thirdparty.CollectibleUniqueID) (map[string]thirdparty.CollectibleData, error) {
|
||||
ret := make(map[string]thirdparty.CollectibleData)
|
||||
|
||||
getData, err := o.db.Prepare(fmt.Sprintf(`SELECT %s
|
||||
FROM collectible_data_cache
|
||||
WHERE chain_id=? AND contract_address=? AND token_id=?`, collectibleDataColumns))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
row := getData.QueryRow(
|
||||
id.ContractID.ChainID,
|
||||
id.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(id.TokenID.Int),
|
||||
)
|
||||
c, err := scanCollectiblesDataRow(row)
|
||||
if err == sql.ErrNoRows {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
// Get traits from different table
|
||||
c.Traits, err = getCollectibleTraits(o.db, c.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get contract type from different table
|
||||
c.ContractType, err = readContractType(o.db, c.ID.ContractID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret[c.ID.HashKey()] = *c
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (o *CollectibleDataDB) SetCommunityInfo(id thirdparty.CollectibleUniqueID, communityInfo thirdparty.CollectibleCommunityInfo) (err error) {
|
||||
tx, err := o.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err == nil {
|
||||
err = tx.Commit()
|
||||
return
|
||||
}
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
update, err := tx.Prepare(`UPDATE collectible_data_cache
|
||||
SET community_privileges_level=?
|
||||
WHERE chain_id=? AND contract_address=? AND token_id=?`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = update.Exec(
|
||||
communityInfo.PrivilegesLevel,
|
||||
id.ContractID.ChainID,
|
||||
id.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(id.TokenID.Int),
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *CollectibleDataDB) GetCommunityInfo(id thirdparty.CollectibleUniqueID) (*thirdparty.CollectibleCommunityInfo, error) {
|
||||
ret := thirdparty.CollectibleCommunityInfo{
|
||||
PrivilegesLevel: token.CommunityLevel,
|
||||
}
|
||||
|
||||
getData, err := o.db.Prepare(fmt.Sprintf(`SELECT %s
|
||||
FROM collectible_data_cache
|
||||
WHERE chain_id=? AND contract_address=? AND token_id=?`, collectibleCommunityDataColumns))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
row := getData.QueryRow(
|
||||
id.ContractID.ChainID,
|
||||
id.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(id.TokenID.Int),
|
||||
)
|
||||
|
||||
var dbPrivilegesLevel sql.NullByte
|
||||
|
||||
err = row.Scan(
|
||||
&dbPrivilegesLevel,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if dbPrivilegesLevel.Valid {
|
||||
ret.PrivilegesLevel = token.PrivilegesLevel(dbPrivilegesLevel.Byte)
|
||||
}
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
Generated
Vendored
+247
@@ -0,0 +1,247 @@
|
||||
package collectibles
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
"github.com/status-im/status-go/sqlite"
|
||||
)
|
||||
|
||||
type CollectionDataDB struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewCollectionDataDB(sqlDb *sql.DB) *CollectionDataDB {
|
||||
return &CollectionDataDB{
|
||||
db: sqlDb,
|
||||
}
|
||||
}
|
||||
|
||||
const collectionDataColumns = "chain_id, contract_address, provider, name, slug, image_url, image_payload, community_id"
|
||||
const collectionTraitsColumns = "chain_id, contract_address, trait_type, min, max"
|
||||
const selectCollectionTraitsColumns = "trait_type, min, max"
|
||||
|
||||
func rowsToCollectionTraits(rows *sql.Rows) (map[string]thirdparty.CollectionTrait, error) {
|
||||
traits := make(map[string]thirdparty.CollectionTrait)
|
||||
for rows.Next() {
|
||||
var traitType string
|
||||
var trait thirdparty.CollectionTrait
|
||||
err := rows.Scan(
|
||||
&traitType,
|
||||
&trait.Min,
|
||||
&trait.Max,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
traits[traitType] = trait
|
||||
}
|
||||
return traits, nil
|
||||
}
|
||||
|
||||
func getCollectionTraits(creator sqlite.StatementCreator, id thirdparty.ContractID) (map[string]thirdparty.CollectionTrait, error) {
|
||||
// Get traits list
|
||||
selectTraits, err := creator.Prepare(fmt.Sprintf(`SELECT %s
|
||||
FROM collection_traits_cache
|
||||
WHERE chain_id = ? AND contract_address = ?`, selectCollectionTraitsColumns))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := selectTraits.Query(
|
||||
id.ChainID,
|
||||
id.Address,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rowsToCollectionTraits(rows)
|
||||
}
|
||||
|
||||
func upsertCollectionTraits(creator sqlite.StatementCreator, id thirdparty.ContractID, traits map[string]thirdparty.CollectionTrait) error {
|
||||
// Rremove old traits list
|
||||
deleteTraits, err := creator.Prepare(`DELETE FROM collection_traits_cache WHERE chain_id = ? AND contract_address = ?`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = deleteTraits.Exec(
|
||||
id.ChainID,
|
||||
id.Address,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert new traits list
|
||||
insertTrait, err := creator.Prepare(fmt.Sprintf(`INSERT OR REPLACE INTO collection_traits_cache (%s)
|
||||
VALUES (?, ?, ?, ?, ?)`, collectionTraitsColumns))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for traitType, trait := range traits {
|
||||
_, err = insertTrait.Exec(
|
||||
id.ChainID,
|
||||
id.Address,
|
||||
traitType,
|
||||
trait.Min,
|
||||
trait.Max,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setCollectionsData(creator sqlite.StatementCreator, collections []thirdparty.CollectionData, allowUpdate bool) error {
|
||||
insertCollection, err := creator.Prepare(fmt.Sprintf(`%s INTO collection_data_cache (%s)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, insertStatement(allowUpdate), collectionDataColumns))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, c := range collections {
|
||||
_, err = insertCollection.Exec(
|
||||
c.ID.ChainID,
|
||||
c.ID.Address,
|
||||
c.Provider,
|
||||
c.Name,
|
||||
c.Slug,
|
||||
c.ImageURL,
|
||||
c.ImagePayload,
|
||||
c.CommunityID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = upsertContractType(creator, c.ID, c.ContractType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if allowUpdate {
|
||||
err = upsertCollectionTraits(creator, c.ID, c.Traits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *CollectionDataDB) SetData(collections []thirdparty.CollectionData, allowUpdate bool) (err error) {
|
||||
tx, err := o.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err == nil {
|
||||
err = tx.Commit()
|
||||
return
|
||||
}
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
// Insert new collections data
|
||||
err = setCollectionsData(tx, collections, allowUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func scanCollectionsDataRow(row *sql.Row) (*thirdparty.CollectionData, error) {
|
||||
c := thirdparty.CollectionData{
|
||||
Traits: make(map[string]thirdparty.CollectionTrait),
|
||||
}
|
||||
err := row.Scan(
|
||||
&c.ID.ChainID,
|
||||
&c.ID.Address,
|
||||
&c.Provider,
|
||||
&c.Name,
|
||||
&c.Slug,
|
||||
&c.ImageURL,
|
||||
&c.ImagePayload,
|
||||
&c.CommunityID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (o *CollectionDataDB) GetIDsNotInDB(ids []thirdparty.ContractID) ([]thirdparty.ContractID, error) {
|
||||
ret := make([]thirdparty.ContractID, 0, len(ids))
|
||||
|
||||
exists, err := o.db.Prepare(`SELECT EXISTS (
|
||||
SELECT 1 FROM collection_data_cache
|
||||
WHERE chain_id=? AND contract_address=?
|
||||
)`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
row := exists.QueryRow(
|
||||
id.ChainID,
|
||||
id.Address,
|
||||
)
|
||||
var exists bool
|
||||
err = row.Scan(&exists)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
ret = append(ret, id)
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (o *CollectionDataDB) GetData(ids []thirdparty.ContractID) (map[string]thirdparty.CollectionData, error) {
|
||||
ret := make(map[string]thirdparty.CollectionData)
|
||||
|
||||
getData, err := o.db.Prepare(fmt.Sprintf(`SELECT %s
|
||||
FROM collection_data_cache
|
||||
WHERE chain_id=? AND contract_address=?`, collectionDataColumns))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
row := getData.QueryRow(
|
||||
id.ChainID,
|
||||
id.Address,
|
||||
)
|
||||
c, err := scanCollectionsDataRow(row)
|
||||
if err == sql.ErrNoRows {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
// Get traits from different table
|
||||
c.Traits, err = getCollectionTraits(o.db, c.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get contract type from different table
|
||||
c.ContractType, err = readContractType(o.db, c.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret[c.ID.HashKey()] = *c
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
package collectibles
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math/big"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/status-im/status-go/services/wallet/async"
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
walletCommon "github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
"github.com/status-im/status-go/services/wallet/transfer"
|
||||
"github.com/status-im/status-go/services/wallet/walletevent"
|
||||
)
|
||||
|
||||
const (
|
||||
fetchLimit = 50 // Limit number of collectibles we fetch per provider call
|
||||
accountOwnershipUpdateInterval = 60 * time.Minute
|
||||
accountOwnershipUpdateDelayInterval = 30 * time.Second
|
||||
)
|
||||
|
||||
type OwnershipState = int
|
||||
|
||||
type OwnedCollectibles struct {
|
||||
chainID walletCommon.ChainID
|
||||
account common.Address
|
||||
ids []thirdparty.CollectibleUniqueID
|
||||
}
|
||||
|
||||
type OwnedCollectiblesChangeType = int
|
||||
|
||||
const (
|
||||
OwnedCollectiblesChangeTypeAdded OwnedCollectiblesChangeType = iota + 1
|
||||
OwnedCollectiblesChangeTypeUpdated
|
||||
OwnedCollectiblesChangeTypeRemoved
|
||||
)
|
||||
|
||||
type OwnedCollectiblesChange struct {
|
||||
ownedCollectibles OwnedCollectibles
|
||||
changeType OwnedCollectiblesChangeType
|
||||
}
|
||||
|
||||
type OwnedCollectiblesChangeCb func(OwnedCollectiblesChange)
|
||||
|
||||
type TransferCb func(common.Address, walletCommon.ChainID, []transfer.Transfer)
|
||||
|
||||
const (
|
||||
OwnershipStateIdle OwnershipState = iota + 1
|
||||
OwnershipStateDelayed
|
||||
OwnershipStateUpdating
|
||||
OwnershipStateError
|
||||
)
|
||||
|
||||
type periodicRefreshOwnedCollectiblesCommand struct {
|
||||
chainID walletCommon.ChainID
|
||||
account common.Address
|
||||
manager *Manager
|
||||
ownershipDB *OwnershipDB
|
||||
walletFeed *event.Feed
|
||||
ownedCollectiblesChangeCb OwnedCollectiblesChangeCb
|
||||
|
||||
group *async.Group
|
||||
state atomic.Value
|
||||
}
|
||||
|
||||
func newPeriodicRefreshOwnedCollectiblesCommand(
|
||||
manager *Manager,
|
||||
ownershipDB *OwnershipDB,
|
||||
walletFeed *event.Feed,
|
||||
chainID walletCommon.ChainID,
|
||||
account common.Address,
|
||||
ownedCollectiblesChangeCb OwnedCollectiblesChangeCb) *periodicRefreshOwnedCollectiblesCommand {
|
||||
ret := &periodicRefreshOwnedCollectiblesCommand{
|
||||
manager: manager,
|
||||
ownershipDB: ownershipDB,
|
||||
walletFeed: walletFeed,
|
||||
chainID: chainID,
|
||||
account: account,
|
||||
ownedCollectiblesChangeCb: ownedCollectiblesChangeCb,
|
||||
}
|
||||
ret.state.Store(OwnershipStateIdle)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *periodicRefreshOwnedCollectiblesCommand) DelayedCommand() async.Command {
|
||||
return async.SingleShotCommand{
|
||||
Interval: accountOwnershipUpdateDelayInterval,
|
||||
Init: func(ctx context.Context) (err error) {
|
||||
c.state.Store(OwnershipStateDelayed)
|
||||
return nil
|
||||
},
|
||||
Runable: c.Command(),
|
||||
}.Run
|
||||
}
|
||||
|
||||
func (c *periodicRefreshOwnedCollectiblesCommand) Command() async.Command {
|
||||
return async.InfiniteCommand{
|
||||
Interval: accountOwnershipUpdateInterval,
|
||||
Runable: c.Run,
|
||||
}.Run
|
||||
}
|
||||
|
||||
func (c *periodicRefreshOwnedCollectiblesCommand) Run(ctx context.Context) (err error) {
|
||||
return c.loadOwnedCollectibles(ctx)
|
||||
}
|
||||
|
||||
func (c *periodicRefreshOwnedCollectiblesCommand) GetState() OwnershipState {
|
||||
return c.state.Load().(OwnershipState)
|
||||
}
|
||||
|
||||
func (c *periodicRefreshOwnedCollectiblesCommand) Stop() {
|
||||
if c.group != nil {
|
||||
c.group.Stop()
|
||||
c.group.Wait()
|
||||
c.group = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *periodicRefreshOwnedCollectiblesCommand) loadOwnedCollectibles(ctx context.Context) error {
|
||||
c.group = async.NewGroup(ctx)
|
||||
|
||||
ownedCollectiblesChangeCh := make(chan OwnedCollectiblesChange)
|
||||
command := newLoadOwnedCollectiblesCommand(c.manager, c.ownershipDB, c.walletFeed, c.chainID, c.account, ownedCollectiblesChangeCh)
|
||||
|
||||
c.state.Store(OwnershipStateUpdating)
|
||||
defer func() {
|
||||
if command.err != nil {
|
||||
c.state.Store(OwnershipStateError)
|
||||
} else {
|
||||
c.state.Store(OwnershipStateIdle)
|
||||
}
|
||||
}()
|
||||
|
||||
c.group.Add(command.Command())
|
||||
|
||||
select {
|
||||
case ownedCollectiblesChange := <-ownedCollectiblesChangeCh:
|
||||
if c.ownedCollectiblesChangeCb != nil {
|
||||
c.ownedCollectiblesChangeCb(ownedCollectiblesChange)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-c.group.WaitAsync():
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetches owned collectibles for a ChainID+OwnerAddress combination in chunks
|
||||
// and updates the ownershipDB when all chunks are loaded
|
||||
type loadOwnedCollectiblesCommand struct {
|
||||
chainID walletCommon.ChainID
|
||||
account common.Address
|
||||
manager *Manager
|
||||
ownershipDB *OwnershipDB
|
||||
walletFeed *event.Feed
|
||||
ownedCollectiblesChangeCh chan<- OwnedCollectiblesChange
|
||||
|
||||
// Not to be set by the caller
|
||||
partialOwnership []thirdparty.CollectibleUniqueID
|
||||
err error
|
||||
}
|
||||
|
||||
func newLoadOwnedCollectiblesCommand(
|
||||
manager *Manager,
|
||||
ownershipDB *OwnershipDB,
|
||||
walletFeed *event.Feed,
|
||||
chainID walletCommon.ChainID,
|
||||
account common.Address,
|
||||
ownedCollectiblesChangeCh chan<- OwnedCollectiblesChange) *loadOwnedCollectiblesCommand {
|
||||
return &loadOwnedCollectiblesCommand{
|
||||
manager: manager,
|
||||
ownershipDB: ownershipDB,
|
||||
walletFeed: walletFeed,
|
||||
chainID: chainID,
|
||||
account: account,
|
||||
ownedCollectiblesChangeCh: ownedCollectiblesChangeCh,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *loadOwnedCollectiblesCommand) Command() async.Command {
|
||||
return c.Run
|
||||
}
|
||||
|
||||
func (c *loadOwnedCollectiblesCommand) triggerEvent(eventType walletevent.EventType, chainID walletCommon.ChainID, account common.Address, message string) {
|
||||
c.walletFeed.Send(walletevent.Event{
|
||||
Type: eventType,
|
||||
ChainID: uint64(chainID),
|
||||
Accounts: []common.Address{
|
||||
account,
|
||||
},
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
func ownedTokensToTokenBalancesPerContractAddress(ownership []thirdparty.CollectibleUniqueID) thirdparty.TokenBalancesPerContractAddress {
|
||||
ret := make(thirdparty.TokenBalancesPerContractAddress)
|
||||
for _, id := range ownership {
|
||||
balance := thirdparty.TokenBalance{
|
||||
TokenID: id.TokenID,
|
||||
Balance: &bigint.BigInt{Int: big.NewInt(1)},
|
||||
}
|
||||
ret[id.ContractID.Address] = append(ret[id.ContractID.Address], balance)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *loadOwnedCollectiblesCommand) sendOwnedCollectiblesChanges(removed, updated, added []thirdparty.CollectibleUniqueID) {
|
||||
if len(removed) > 0 {
|
||||
c.ownedCollectiblesChangeCh <- OwnedCollectiblesChange{
|
||||
ownedCollectibles: OwnedCollectibles{
|
||||
chainID: c.chainID,
|
||||
account: c.account,
|
||||
ids: removed,
|
||||
},
|
||||
changeType: OwnedCollectiblesChangeTypeRemoved,
|
||||
}
|
||||
}
|
||||
|
||||
if len(updated) > 0 {
|
||||
c.ownedCollectiblesChangeCh <- OwnedCollectiblesChange{
|
||||
ownedCollectibles: OwnedCollectibles{
|
||||
chainID: c.chainID,
|
||||
account: c.account,
|
||||
ids: updated,
|
||||
},
|
||||
changeType: OwnedCollectiblesChangeTypeUpdated,
|
||||
}
|
||||
}
|
||||
|
||||
if len(added) > 0 {
|
||||
c.ownedCollectiblesChangeCh <- OwnedCollectiblesChange{
|
||||
ownedCollectibles: OwnedCollectibles{
|
||||
chainID: c.chainID,
|
||||
account: c.account,
|
||||
ids: added,
|
||||
},
|
||||
changeType: OwnedCollectiblesChangeTypeAdded,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *loadOwnedCollectiblesCommand) Run(parent context.Context) (err error) {
|
||||
log.Debug("start loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account)
|
||||
|
||||
pageNr := 0
|
||||
cursor := thirdparty.FetchFromStartCursor
|
||||
providerID := thirdparty.FetchFromAnyProvider
|
||||
start := time.Now()
|
||||
|
||||
c.triggerEvent(EventCollectiblesOwnershipUpdateStarted, c.chainID, c.account, "")
|
||||
|
||||
updateMessage := OwnershipUpdateMessage{}
|
||||
|
||||
lastFetchTimestamp, err := c.ownershipDB.GetOwnershipUpdateTimestamp(c.account, c.chainID)
|
||||
if err != nil {
|
||||
c.err = err
|
||||
} else {
|
||||
initialFetch := lastFetchTimestamp == InvalidTimestamp
|
||||
// Fetch collectibles in chunks
|
||||
for {
|
||||
if walletCommon.ShouldCancel(parent) {
|
||||
c.err = errors.New("context cancelled")
|
||||
break
|
||||
}
|
||||
|
||||
pageStart := time.Now()
|
||||
log.Debug("start loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "page", pageNr)
|
||||
|
||||
partialOwnership, err := c.manager.FetchCollectibleOwnershipByOwner(parent, c.chainID, c.account, cursor, fetchLimit, providerID)
|
||||
|
||||
if err != nil {
|
||||
log.Error("failed loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "page", pageNr, "error", err)
|
||||
c.err = err
|
||||
break
|
||||
}
|
||||
|
||||
log.Debug("partial loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "page", pageNr, "in", time.Since(pageStart), "found", len(partialOwnership.Items))
|
||||
|
||||
c.partialOwnership = append(c.partialOwnership, partialOwnership.Items...)
|
||||
|
||||
pageNr++
|
||||
cursor = partialOwnership.NextCursor
|
||||
providerID = partialOwnership.Provider
|
||||
|
||||
finished := cursor == thirdparty.FetchFromStartCursor
|
||||
|
||||
// Normally, update the DB once we've finished fetching
|
||||
// If this is the first fetch, make partial updates to the client to get a better UX
|
||||
if initialFetch || finished {
|
||||
// Token balances should come from the providers. For now we assume all balances are 1, which
|
||||
// is only valid for ERC721.
|
||||
// TODO (#13025): Fetch balances from the providers.
|
||||
balances := ownedTokensToTokenBalancesPerContractAddress(c.partialOwnership)
|
||||
|
||||
updateMessage.Removed, updateMessage.Updated, updateMessage.Added, err = c.ownershipDB.Update(c.chainID, c.account, balances, start.Unix())
|
||||
if err != nil {
|
||||
log.Error("failed updating ownershipDB in loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "error", err)
|
||||
c.err = err
|
||||
break
|
||||
}
|
||||
|
||||
c.sendOwnedCollectiblesChanges(updateMessage.Removed, updateMessage.Updated, updateMessage.Added)
|
||||
}
|
||||
|
||||
if finished || c.err != nil {
|
||||
break
|
||||
} else if initialFetch {
|
||||
encodedMessage, err := json.Marshal(updateMessage)
|
||||
if err != nil {
|
||||
c.err = err
|
||||
break
|
||||
}
|
||||
c.triggerEvent(EventCollectiblesOwnershipUpdatePartial, c.chainID, c.account, string(encodedMessage))
|
||||
|
||||
updateMessage = OwnershipUpdateMessage{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var encodedMessage []byte
|
||||
if c.err == nil {
|
||||
encodedMessage, c.err = json.Marshal(updateMessage)
|
||||
}
|
||||
|
||||
if c.err != nil {
|
||||
c.triggerEvent(EventCollectiblesOwnershipUpdateFinishedWithError, c.chainID, c.account, c.err.Error())
|
||||
} else {
|
||||
c.triggerEvent(EventCollectiblesOwnershipUpdateFinished, c.chainID, c.account, string(encodedMessage))
|
||||
}
|
||||
|
||||
log.Debug("end loadOwnedCollectiblesCommand", "chain", c.chainID, "account", c.account, "in", time.Since(start))
|
||||
return nil
|
||||
}
|
||||
Generated
Vendored
+68
@@ -0,0 +1,68 @@
|
||||
package collectibles
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
|
||||
w_common "github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
"github.com/status-im/status-go/sqlite"
|
||||
)
|
||||
|
||||
func upsertContractType(creator sqlite.StatementCreator, id thirdparty.ContractID, contractType w_common.ContractType) error {
|
||||
if contractType == w_common.ContractTypeUnknown {
|
||||
return nil
|
||||
}
|
||||
|
||||
q := sq.Replace("contract_type_cache").
|
||||
SetMap(sq.Eq{"chain_id": id.ChainID, "contract_address": id.Address, "contract_type": contractType})
|
||||
|
||||
query, args, err := q.ToSql()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stmt, err := creator.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
_, err = stmt.Exec(args...)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func readContractType(creator sqlite.StatementCreator, id thirdparty.ContractID) (w_common.ContractType, error) {
|
||||
q := sq.Select("contract_type").
|
||||
From("contract_type_cache").
|
||||
Where(sq.Eq{"chain_id": id.ChainID, "contract_address": id.Address})
|
||||
|
||||
query, args, err := q.ToSql()
|
||||
if err != nil {
|
||||
return w_common.ContractTypeUnknown, err
|
||||
}
|
||||
|
||||
stmt, err := creator.Prepare(query)
|
||||
if err != nil {
|
||||
return w_common.ContractTypeUnknown, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
_, err = stmt.Exec(args...)
|
||||
if err != nil {
|
||||
return w_common.ContractTypeUnknown, err
|
||||
}
|
||||
|
||||
var transferType w_common.ContractType
|
||||
err = stmt.QueryRow(args...).Scan(&transferType)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return w_common.ContractTypeUnknown, nil
|
||||
} else if err != nil {
|
||||
return w_common.ContractTypeUnknown, err
|
||||
}
|
||||
|
||||
return transferType, nil
|
||||
}
|
||||
+418
@@ -0,0 +1,418 @@
|
||||
package collectibles
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/status-im/status-go/multiaccounts/accounts"
|
||||
"github.com/status-im/status-go/multiaccounts/settings"
|
||||
"github.com/status-im/status-go/rpc/network"
|
||||
"github.com/status-im/status-go/services/accounts/accountsevent"
|
||||
"github.com/status-im/status-go/services/accounts/settingsevent"
|
||||
"github.com/status-im/status-go/services/wallet/async"
|
||||
walletCommon "github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/transfer"
|
||||
"github.com/status-im/status-go/services/wallet/walletevent"
|
||||
)
|
||||
|
||||
const (
|
||||
activityRefetchMarginSeconds = 30 * 60 // Trigger a fetch if activity is detected this many seconds before the last fetch
|
||||
)
|
||||
|
||||
type commandPerChainID = map[walletCommon.ChainID]*periodicRefreshOwnedCollectiblesCommand
|
||||
type commandPerAddressAndChainID = map[common.Address]commandPerChainID
|
||||
|
||||
type timerPerChainID = map[walletCommon.ChainID]*time.Timer
|
||||
type timerPerAddressAndChainID = map[common.Address]timerPerChainID
|
||||
|
||||
type Controller struct {
|
||||
manager *Manager
|
||||
ownershipDB *OwnershipDB
|
||||
walletFeed *event.Feed
|
||||
accountsDB *accounts.Database
|
||||
accountsFeed *event.Feed
|
||||
settingsFeed *event.Feed
|
||||
|
||||
networkManager *network.Manager
|
||||
cancelFn context.CancelFunc
|
||||
|
||||
commands commandPerAddressAndChainID
|
||||
timers timerPerAddressAndChainID
|
||||
group *async.Group
|
||||
accountsWatcher *accountsevent.Watcher
|
||||
walletEventsWatcher *walletevent.Watcher
|
||||
settingsWatcher *settingsevent.Watcher
|
||||
|
||||
ownedCollectiblesChangeCb OwnedCollectiblesChangeCb
|
||||
collectiblesTransferCb TransferCb
|
||||
|
||||
commandsLock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewController(
|
||||
db *sql.DB,
|
||||
walletFeed *event.Feed,
|
||||
accountsDB *accounts.Database,
|
||||
accountsFeed *event.Feed,
|
||||
settingsFeed *event.Feed,
|
||||
networkManager *network.Manager,
|
||||
manager *Manager) *Controller {
|
||||
return &Controller{
|
||||
manager: manager,
|
||||
ownershipDB: NewOwnershipDB(db),
|
||||
walletFeed: walletFeed,
|
||||
accountsDB: accountsDB,
|
||||
accountsFeed: accountsFeed,
|
||||
settingsFeed: settingsFeed,
|
||||
networkManager: networkManager,
|
||||
commands: make(commandPerAddressAndChainID),
|
||||
timers: make(timerPerAddressAndChainID),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) SetOwnedCollectiblesChangeCb(cb OwnedCollectiblesChangeCb) {
|
||||
c.ownedCollectiblesChangeCb = cb
|
||||
}
|
||||
|
||||
func (c *Controller) SetCollectiblesTransferCb(cb TransferCb) {
|
||||
c.collectiblesTransferCb = cb
|
||||
}
|
||||
|
||||
func (c *Controller) Start() {
|
||||
// Setup periodical collectibles refresh
|
||||
_ = c.startPeriodicalOwnershipFetch()
|
||||
|
||||
// Setup collectibles fetch when a new account gets added
|
||||
c.startAccountsWatcher()
|
||||
|
||||
// Setup collectibles fetch when relevant activity is detected
|
||||
c.startWalletEventsWatcher()
|
||||
|
||||
// Setup collectibles fetch when chain-related settings change
|
||||
c.startSettingsWatcher()
|
||||
}
|
||||
|
||||
func (c *Controller) Stop() {
|
||||
c.stopSettingsWatcher()
|
||||
|
||||
c.stopWalletEventsWatcher()
|
||||
|
||||
c.stopAccountsWatcher()
|
||||
|
||||
c.stopPeriodicalOwnershipFetch()
|
||||
}
|
||||
|
||||
func (c *Controller) RefetchOwnedCollectibles() {
|
||||
c.stopPeriodicalOwnershipFetch()
|
||||
c.manager.ResetConnectionStatus()
|
||||
_ = c.startPeriodicalOwnershipFetch()
|
||||
}
|
||||
|
||||
func (c *Controller) GetCommandState(chainID walletCommon.ChainID, address common.Address) OwnershipState {
|
||||
c.commandsLock.RLock()
|
||||
defer c.commandsLock.RUnlock()
|
||||
|
||||
state := OwnershipStateIdle
|
||||
if c.commands[address] != nil && c.commands[address][chainID] != nil {
|
||||
state = c.commands[address][chainID].GetState()
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
func (c *Controller) isPeriodicalOwnershipFetchRunning() bool {
|
||||
return c.group != nil
|
||||
}
|
||||
|
||||
// Starts periodical fetching for the all wallet addresses and all chains
|
||||
func (c *Controller) startPeriodicalOwnershipFetch() error {
|
||||
c.commandsLock.Lock()
|
||||
defer c.commandsLock.Unlock()
|
||||
|
||||
if c.isPeriodicalOwnershipFetchRunning() {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c.cancelFn = cancel
|
||||
|
||||
c.group = async.NewGroup(ctx)
|
||||
|
||||
addresses, err := c.accountsDB.GetWalletAddresses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, addr := range addresses {
|
||||
err := c.startPeriodicalOwnershipFetchForAccount(common.Address(addr))
|
||||
if err != nil {
|
||||
log.Error("Error starting periodical collectibles fetch for accpunt", "address", addr, "error", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) stopPeriodicalOwnershipFetch() {
|
||||
c.commandsLock.Lock()
|
||||
defer c.commandsLock.Unlock()
|
||||
|
||||
if !c.isPeriodicalOwnershipFetchRunning() {
|
||||
return
|
||||
}
|
||||
|
||||
if c.cancelFn != nil {
|
||||
c.cancelFn()
|
||||
c.cancelFn = nil
|
||||
}
|
||||
if c.group != nil {
|
||||
c.group.Stop()
|
||||
c.group.Wait()
|
||||
c.group = nil
|
||||
c.commands = make(commandPerAddressAndChainID)
|
||||
}
|
||||
}
|
||||
|
||||
// Starts (or restarts) periodical fetching for the given account address for all chains
|
||||
func (c *Controller) startPeriodicalOwnershipFetchForAccount(address common.Address) error {
|
||||
log.Debug("wallet.api.collectibles.Controller Start periodical fetching", "address", address)
|
||||
|
||||
networks, err := c.networkManager.Get(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
areTestNetworksEnabled, err := c.accountsDB.GetTestNetworksEnabled()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, network := range networks {
|
||||
if network.IsTest != areTestNetworksEnabled {
|
||||
continue
|
||||
}
|
||||
chainID := walletCommon.ChainID(network.ChainID)
|
||||
|
||||
err := c.startPeriodicalOwnershipFetchForAccountAndChainID(address, chainID, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Starts (or restarts) periodical fetching for the given account address for all chains
|
||||
func (c *Controller) startPeriodicalOwnershipFetchForAccountAndChainID(address common.Address, chainID walletCommon.ChainID, delayed bool) error {
|
||||
log.Debug("wallet.api.collectibles.Controller Start periodical fetching", "address", address, "chainID", chainID, "delayed", delayed)
|
||||
|
||||
if !c.isPeriodicalOwnershipFetchRunning() {
|
||||
return errors.New("periodical fetch not initialized")
|
||||
}
|
||||
|
||||
err := c.stopPeriodicalOwnershipFetchForAccountAndChainID(address, chainID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := c.commands[address]; !ok {
|
||||
c.commands[address] = make(commandPerChainID)
|
||||
}
|
||||
|
||||
command := newPeriodicRefreshOwnedCollectiblesCommand(
|
||||
c.manager,
|
||||
c.ownershipDB,
|
||||
c.walletFeed,
|
||||
chainID,
|
||||
address,
|
||||
c.ownedCollectiblesChangeCb,
|
||||
)
|
||||
|
||||
c.commands[address][chainID] = command
|
||||
if delayed {
|
||||
c.group.Add(command.DelayedCommand())
|
||||
} else {
|
||||
c.group.Add(command.Command())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop periodical fetching for the given account address for all chains
|
||||
func (c *Controller) stopPeriodicalOwnershipFetchForAccount(address common.Address) error {
|
||||
log.Debug("wallet.api.collectibles.Controller Stop periodical fetching", "address", address)
|
||||
|
||||
if !c.isPeriodicalOwnershipFetchRunning() {
|
||||
return errors.New("periodical fetch not initialized")
|
||||
}
|
||||
|
||||
if _, ok := c.commands[address]; ok {
|
||||
for chainID := range c.commands[address] {
|
||||
err := c.stopPeriodicalOwnershipFetchForAccountAndChainID(address, chainID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) stopPeriodicalOwnershipFetchForAccountAndChainID(address common.Address, chainID walletCommon.ChainID) error {
|
||||
log.Debug("wallet.api.collectibles.Controller Stop periodical fetching", "address", address, "chainID", chainID)
|
||||
|
||||
if !c.isPeriodicalOwnershipFetchRunning() {
|
||||
return errors.New("periodical fetch not initialized")
|
||||
}
|
||||
|
||||
if _, ok := c.commands[address]; ok {
|
||||
if _, ok := c.commands[address][chainID]; ok {
|
||||
c.commands[address][chainID].Stop()
|
||||
delete(c.commands[address], chainID)
|
||||
}
|
||||
// If it was the last chain, delete the address as well
|
||||
if len(c.commands[address]) == 0 {
|
||||
delete(c.commands, address)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) startAccountsWatcher() {
|
||||
if c.accountsWatcher != nil {
|
||||
return
|
||||
}
|
||||
|
||||
accountChangeCb := func(changedAddresses []common.Address, eventType accountsevent.EventType, currentAddresses []common.Address) {
|
||||
c.commandsLock.Lock()
|
||||
defer c.commandsLock.Unlock()
|
||||
// Whenever an account gets added, start fetching
|
||||
if eventType == accountsevent.EventTypeAdded {
|
||||
for _, address := range changedAddresses {
|
||||
err := c.startPeriodicalOwnershipFetchForAccount(address)
|
||||
if err != nil {
|
||||
log.Error("Error starting periodical collectibles fetch", "address", address, "error", err)
|
||||
}
|
||||
}
|
||||
} else if eventType == accountsevent.EventTypeRemoved {
|
||||
for _, address := range changedAddresses {
|
||||
err := c.stopPeriodicalOwnershipFetchForAccount(address)
|
||||
if err != nil {
|
||||
log.Error("Error starting periodical collectibles fetch", "address", address, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.accountsWatcher = accountsevent.NewWatcher(c.accountsDB, c.accountsFeed, accountChangeCb)
|
||||
|
||||
c.accountsWatcher.Start()
|
||||
}
|
||||
|
||||
func (c *Controller) stopAccountsWatcher() {
|
||||
if c.accountsWatcher != nil {
|
||||
c.accountsWatcher.Stop()
|
||||
c.accountsWatcher = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) startWalletEventsWatcher() {
|
||||
if c.walletEventsWatcher != nil {
|
||||
return
|
||||
}
|
||||
|
||||
walletEventCb := func(event walletevent.Event) {
|
||||
// EventRecentHistoryReady ?
|
||||
if event.Type != transfer.EventInternalERC721TransferDetected &&
|
||||
event.Type != transfer.EventInternalERC1155TransferDetected {
|
||||
return
|
||||
}
|
||||
|
||||
chainID := walletCommon.ChainID(event.ChainID)
|
||||
for _, account := range event.Accounts {
|
||||
// Call external callback
|
||||
if c.collectiblesTransferCb != nil {
|
||||
c.collectiblesTransferCb(account, chainID, event.EventParams.([]transfer.Transfer))
|
||||
}
|
||||
|
||||
c.refetchOwnershipIfRecentTransfer(account, chainID, event.At)
|
||||
}
|
||||
}
|
||||
|
||||
c.walletEventsWatcher = walletevent.NewWatcher(c.walletFeed, walletEventCb)
|
||||
|
||||
c.walletEventsWatcher.Start()
|
||||
}
|
||||
|
||||
func (c *Controller) stopWalletEventsWatcher() {
|
||||
if c.walletEventsWatcher != nil {
|
||||
c.walletEventsWatcher.Stop()
|
||||
c.walletEventsWatcher = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) startSettingsWatcher() {
|
||||
if c.settingsWatcher != nil {
|
||||
return
|
||||
}
|
||||
|
||||
settingChangeCb := func(setting settings.SettingField, value interface{}) {
|
||||
if setting.Equals(settings.TestNetworksEnabled) || setting.Equals(settings.IsSepoliaEnabled) {
|
||||
c.stopPeriodicalOwnershipFetch()
|
||||
err := c.startPeriodicalOwnershipFetch()
|
||||
if err != nil {
|
||||
log.Error("Error starting periodical collectibles fetch", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.settingsWatcher = settingsevent.NewWatcher(c.settingsFeed, settingChangeCb)
|
||||
|
||||
c.settingsWatcher.Start()
|
||||
}
|
||||
|
||||
func (c *Controller) stopSettingsWatcher() {
|
||||
if c.settingsWatcher != nil {
|
||||
c.settingsWatcher.Stop()
|
||||
c.settingsWatcher = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) refetchOwnershipIfRecentTransfer(account common.Address, chainID walletCommon.ChainID, latestTxTimestamp int64) {
|
||||
|
||||
// Check last ownership update timestamp
|
||||
timestamp, err := c.ownershipDB.GetOwnershipUpdateTimestamp(account, chainID)
|
||||
|
||||
if err != nil {
|
||||
log.Error("Error getting ownership update timestamp", "error", err)
|
||||
return
|
||||
}
|
||||
if timestamp == InvalidTimestamp {
|
||||
// Ownership was never fetched for this account
|
||||
return
|
||||
}
|
||||
|
||||
timeCheck := timestamp - activityRefetchMarginSeconds
|
||||
if timeCheck < 0 {
|
||||
timeCheck = 0
|
||||
}
|
||||
|
||||
if latestTxTimestamp > timeCheck {
|
||||
// Restart fetching for account + chainID
|
||||
c.commandsLock.Lock()
|
||||
err := c.startPeriodicalOwnershipFetchForAccountAndChainID(account, chainID, true)
|
||||
c.commandsLock.Unlock()
|
||||
if err != nil {
|
||||
log.Error("Error starting periodical collectibles fetch", "address", account, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package collectibles
|
||||
|
||||
func insertStatement(allowUpdate bool) string {
|
||||
if allowUpdate {
|
||||
return `INSERT OR REPLACE`
|
||||
}
|
||||
return `INSERT OR IGNORE`
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package collectibles
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
|
||||
"github.com/status-im/status-go/protocol/communities/token"
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
wcommon "github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
)
|
||||
|
||||
func allCollectibleIDsFilter() []thirdparty.CollectibleUniqueID {
|
||||
return []thirdparty.CollectibleUniqueID{}
|
||||
}
|
||||
|
||||
func allCommunityIDsFilter() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func allCommunityPrivilegesLevelsFilter() []token.PrivilegesLevel {
|
||||
return []token.PrivilegesLevel{}
|
||||
}
|
||||
|
||||
func allFilter() Filter {
|
||||
return Filter{
|
||||
CollectibleIDs: allCollectibleIDsFilter(),
|
||||
CommunityIDs: allCommunityIDsFilter(),
|
||||
CommunityPrivilegesLevels: allCommunityPrivilegesLevelsFilter(),
|
||||
FilterCommunity: All,
|
||||
}
|
||||
}
|
||||
|
||||
type FilterCommunityType int
|
||||
|
||||
const (
|
||||
All FilterCommunityType = iota
|
||||
OnlyNonCommunity
|
||||
OnlyCommunity
|
||||
)
|
||||
|
||||
type Filter struct {
|
||||
CollectibleIDs []thirdparty.CollectibleUniqueID `json:"collectible_ids"`
|
||||
CommunityIDs []string `json:"community_ids"`
|
||||
CommunityPrivilegesLevels []token.PrivilegesLevel `json:"community_privileges_levels"`
|
||||
|
||||
FilterCommunity FilterCommunityType `json:"filter_community"`
|
||||
}
|
||||
|
||||
func filterOwnedCollectibles(ctx context.Context, db *sql.DB, chainIDs []wcommon.ChainID, addresses []common.Address, filter Filter, offset int, limit int) ([]thirdparty.CollectibleUniqueID, error) {
|
||||
if len(addresses) == 0 {
|
||||
return nil, errors.New("no addresses provided")
|
||||
}
|
||||
if len(chainIDs) == 0 {
|
||||
return nil, errors.New("no chainIDs provided")
|
||||
}
|
||||
|
||||
q := sq.Select("ownership.chain_id,ownership.contract_address,ownership.token_id")
|
||||
q = q.From("collectibles_ownership_cache ownership").
|
||||
LeftJoin(`collectible_data_cache data ON
|
||||
ownership.chain_id = data.chain_id AND
|
||||
ownership.contract_address = data.contract_address AND
|
||||
ownership.token_id = data.token_id`)
|
||||
|
||||
qConditions := sq.And{}
|
||||
qConditions = append(qConditions, sq.Eq{"ownership.chain_id": chainIDs})
|
||||
qConditions = append(qConditions, sq.Eq{"ownership.owner_address": addresses})
|
||||
|
||||
if len(filter.CollectibleIDs) > 0 {
|
||||
collectibleIDConditions := sq.Or{}
|
||||
for _, collectibleID := range filter.CollectibleIDs {
|
||||
collectibleIDConditions = append(collectibleIDConditions,
|
||||
sq.And{
|
||||
sq.Eq{"ownership.chain_id": collectibleID.ContractID.ChainID},
|
||||
sq.Eq{"ownership.contract_address": collectibleID.ContractID.Address},
|
||||
sq.Eq{"ownership.token_id": (*bigint.SQLBigIntBytes)(collectibleID.TokenID.Int)},
|
||||
})
|
||||
}
|
||||
qConditions = append(qConditions, collectibleIDConditions)
|
||||
}
|
||||
|
||||
switch filter.FilterCommunity {
|
||||
case All:
|
||||
// nothing to do
|
||||
case OnlyNonCommunity:
|
||||
qConditions = append(qConditions, sq.Eq{"data.community_id": ""})
|
||||
case OnlyCommunity:
|
||||
qConditions = append(qConditions, sq.NotEq{"data.community_id": ""})
|
||||
}
|
||||
|
||||
if len(filter.CommunityIDs) > 0 {
|
||||
qConditions = append(qConditions, sq.Eq{"data.community_id": filter.CommunityIDs})
|
||||
}
|
||||
|
||||
if len(filter.CommunityPrivilegesLevels) > 0 {
|
||||
qConditions = append(qConditions, sq.Eq{"data.community_privileges_level": filter.CommunityPrivilegesLevels})
|
||||
}
|
||||
|
||||
q = q.Where(qConditions)
|
||||
|
||||
q = q.Limit(uint64(limit))
|
||||
q = q.Offset(uint64(offset))
|
||||
|
||||
query, args, err := q.ToSql()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stmt, err := db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
rows, err := stmt.Query(args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return thirdparty.RowsToCollectibles(rows)
|
||||
}
|
||||
+870
@@ -0,0 +1,870 @@
|
||||
package collectibles
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/afex/hystrix-go/hystrix"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/status-im/status-go/contracts/community-tokens/collectibles"
|
||||
"github.com/status-im/status-go/rpc"
|
||||
"github.com/status-im/status-go/server"
|
||||
"github.com/status-im/status-go/services/wallet/async"
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
walletCommon "github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/community"
|
||||
"github.com/status-im/status-go/services/wallet/connection"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
"github.com/status-im/status-go/services/wallet/walletevent"
|
||||
)
|
||||
|
||||
const requestTimeout = 5 * time.Second
|
||||
const signalUpdatedCollectiblesDataPageSize = 10
|
||||
|
||||
const hystrixContractOwnershipClientName = "contractOwnershipClient"
|
||||
|
||||
const EventCollectiblesConnectionStatusChanged walletevent.EventType = "wallet-collectible-status-changed"
|
||||
|
||||
// ERC721 does not support function "TokenURI" if call
|
||||
// returns error starting with one of these strings
|
||||
var noTokenURIErrorPrefixes = []string{
|
||||
"execution reverted",
|
||||
"abi: attempting to unmarshall",
|
||||
}
|
||||
|
||||
var (
|
||||
ErrAllProvidersFailedForChainID = errors.New("all providers failed for chainID")
|
||||
ErrNoProvidersAvailableForChainID = errors.New("no providers available for chainID")
|
||||
)
|
||||
|
||||
type ManagerInterface interface {
|
||||
FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID, asyncFetch bool) ([]thirdparty.FullCollectibleData, error)
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
rpcClient *rpc.Client
|
||||
contractOwnershipProviders []thirdparty.CollectibleContractOwnershipProvider
|
||||
accountOwnershipProviders []thirdparty.CollectibleAccountOwnershipProvider
|
||||
collectibleDataProviders []thirdparty.CollectibleDataProvider
|
||||
collectionDataProviders []thirdparty.CollectionDataProvider
|
||||
collectibleProviders []thirdparty.CollectibleProvider
|
||||
|
||||
httpClient *http.Client
|
||||
|
||||
collectiblesDataDB *CollectibleDataDB
|
||||
collectionsDataDB *CollectionDataDB
|
||||
communityManager *community.Manager
|
||||
ownershipDB *OwnershipDB
|
||||
|
||||
mediaServer *server.MediaServer
|
||||
|
||||
statuses map[string]*connection.Status
|
||||
statusNotifier *connection.StatusNotifier
|
||||
feed *event.Feed
|
||||
}
|
||||
|
||||
func NewManager(
|
||||
db *sql.DB,
|
||||
rpcClient *rpc.Client,
|
||||
communityManager *community.Manager,
|
||||
contractOwnershipProviders []thirdparty.CollectibleContractOwnershipProvider,
|
||||
accountOwnershipProviders []thirdparty.CollectibleAccountOwnershipProvider,
|
||||
collectibleDataProviders []thirdparty.CollectibleDataProvider,
|
||||
collectionDataProviders []thirdparty.CollectionDataProvider,
|
||||
mediaServer *server.MediaServer,
|
||||
feed *event.Feed) *Manager {
|
||||
hystrix.ConfigureCommand(hystrixContractOwnershipClientName, hystrix.CommandConfig{
|
||||
Timeout: 10000,
|
||||
MaxConcurrentRequests: 100,
|
||||
SleepWindow: 300000,
|
||||
ErrorPercentThreshold: 25,
|
||||
})
|
||||
|
||||
ownershipDB := NewOwnershipDB(db)
|
||||
|
||||
statuses := make(map[string]*connection.Status)
|
||||
|
||||
allChainIDs := walletCommon.AllChainIDs()
|
||||
for _, chainID := range allChainIDs {
|
||||
status := connection.NewStatus()
|
||||
state := status.GetState()
|
||||
latestUpdateTimestamp, err := ownershipDB.GetLatestOwnershipUpdateTimestamp(chainID)
|
||||
if err == nil {
|
||||
state.LastSuccessAt = latestUpdateTimestamp
|
||||
status.SetState(state)
|
||||
}
|
||||
statuses[chainID.String()] = status
|
||||
}
|
||||
|
||||
statusNotifier := connection.NewStatusNotifier(
|
||||
statuses,
|
||||
EventCollectiblesConnectionStatusChanged,
|
||||
feed,
|
||||
)
|
||||
|
||||
// Get list of all providers
|
||||
collectibleProvidersMap := make(map[string]thirdparty.CollectibleProvider)
|
||||
collectibleProviders := make([]thirdparty.CollectibleProvider, 0)
|
||||
for _, provider := range contractOwnershipProviders {
|
||||
collectibleProvidersMap[provider.ID()] = provider
|
||||
}
|
||||
for _, provider := range accountOwnershipProviders {
|
||||
collectibleProvidersMap[provider.ID()] = provider
|
||||
}
|
||||
for _, provider := range collectibleDataProviders {
|
||||
collectibleProvidersMap[provider.ID()] = provider
|
||||
}
|
||||
for _, provider := range collectionDataProviders {
|
||||
collectibleProvidersMap[provider.ID()] = provider
|
||||
}
|
||||
for _, provider := range collectibleProvidersMap {
|
||||
collectibleProviders = append(collectibleProviders, provider)
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
rpcClient: rpcClient,
|
||||
contractOwnershipProviders: contractOwnershipProviders,
|
||||
accountOwnershipProviders: accountOwnershipProviders,
|
||||
collectibleDataProviders: collectibleDataProviders,
|
||||
collectionDataProviders: collectionDataProviders,
|
||||
collectibleProviders: collectibleProviders,
|
||||
httpClient: &http.Client{
|
||||
Timeout: requestTimeout,
|
||||
},
|
||||
collectiblesDataDB: NewCollectibleDataDB(db),
|
||||
collectionsDataDB: NewCollectionDataDB(db),
|
||||
communityManager: communityManager,
|
||||
ownershipDB: ownershipDB,
|
||||
mediaServer: mediaServer,
|
||||
statuses: statuses,
|
||||
statusNotifier: statusNotifier,
|
||||
feed: feed,
|
||||
}
|
||||
}
|
||||
|
||||
func mapToList[K comparable, T any](m map[K]T) []T {
|
||||
list := make([]T, 0, len(m))
|
||||
for _, v := range m {
|
||||
list = append(list, v)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func makeContractOwnershipCall(main func() (any, error), fallback func() (any, error)) (any, error) {
|
||||
resultChan := make(chan any, 1)
|
||||
errChan := hystrix.Go(hystrixContractOwnershipClientName, func() error {
|
||||
res, err := main()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resultChan <- res
|
||||
return nil
|
||||
}, func(err error) error {
|
||||
if fallback == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := fallback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resultChan <- res
|
||||
return nil
|
||||
})
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
return result, nil
|
||||
case err := <-errChan:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Manager) doContentTypeRequest(ctx context.Context, url string) (string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := o.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
log.Error("failed to close head request body", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return resp.Header.Get("Content-Type"), nil
|
||||
}
|
||||
|
||||
// Need to combine different providers to support all needed ChainIDs
|
||||
func (o *Manager) FetchBalancesByOwnerAndContractAddress(ctx context.Context, chainID walletCommon.ChainID, ownerAddress common.Address, contractAddresses []common.Address) (thirdparty.TokenBalancesPerContractAddress, error) {
|
||||
ret := make(thirdparty.TokenBalancesPerContractAddress)
|
||||
|
||||
for _, contractAddress := range contractAddresses {
|
||||
ret[contractAddress] = make([]thirdparty.TokenBalance, 0)
|
||||
}
|
||||
|
||||
// Try with account ownership providers first
|
||||
assetsContainer, err := o.FetchAllAssetsByOwnerAndContractAddress(ctx, chainID, ownerAddress, contractAddresses, thirdparty.FetchFromStartCursor, thirdparty.FetchNoLimit, thirdparty.FetchFromAnyProvider)
|
||||
if err == ErrNoProvidersAvailableForChainID {
|
||||
// Use contract ownership providers
|
||||
for _, contractAddress := range contractAddresses {
|
||||
ownership, err := o.FetchCollectibleOwnersByContractAddress(ctx, chainID, contractAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, nftOwner := range ownership.Owners {
|
||||
if nftOwner.OwnerAddress == ownerAddress {
|
||||
ret[contractAddress] = nftOwner.TokenBalances
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if err == nil {
|
||||
// Account ownership providers succeeded
|
||||
for _, fullData := range assetsContainer.Items {
|
||||
contractAddress := fullData.CollectibleData.ID.ContractID.Address
|
||||
balance := thirdparty.TokenBalance{
|
||||
TokenID: fullData.CollectibleData.ID.TokenID,
|
||||
Balance: &bigint.BigInt{Int: big.NewInt(1)},
|
||||
}
|
||||
ret[contractAddress] = append(ret[contractAddress], balance)
|
||||
}
|
||||
} else {
|
||||
// OpenSea could have provided, but returned error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (o *Manager) FetchAllAssetsByOwnerAndContractAddress(ctx context.Context, chainID walletCommon.ChainID, owner common.Address, contractAddresses []common.Address, cursor string, limit int, providerID string) (*thirdparty.FullCollectibleDataContainer, error) {
|
||||
defer o.checkConnectionStatus(chainID)
|
||||
|
||||
anyProviderAvailable := false
|
||||
for _, provider := range o.accountOwnershipProviders {
|
||||
if !provider.IsChainSupported(chainID) {
|
||||
continue
|
||||
}
|
||||
anyProviderAvailable = true
|
||||
if providerID != thirdparty.FetchFromAnyProvider && providerID != provider.ID() {
|
||||
continue
|
||||
}
|
||||
|
||||
assetContainer, err := provider.FetchAllAssetsByOwnerAndContractAddress(ctx, chainID, owner, contractAddresses, cursor, limit)
|
||||
if err != nil {
|
||||
log.Error("FetchAllAssetsByOwnerAndContractAddress failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = o.processFullCollectibleData(ctx, assetContainer.Items, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return assetContainer, nil
|
||||
}
|
||||
|
||||
if anyProviderAvailable {
|
||||
return nil, ErrAllProvidersFailedForChainID
|
||||
}
|
||||
return nil, ErrNoProvidersAvailableForChainID
|
||||
}
|
||||
|
||||
func (o *Manager) FetchAllAssetsByOwner(ctx context.Context, chainID walletCommon.ChainID, owner common.Address, cursor string, limit int, providerID string) (*thirdparty.FullCollectibleDataContainer, error) {
|
||||
defer o.checkConnectionStatus(chainID)
|
||||
|
||||
anyProviderAvailable := false
|
||||
for _, provider := range o.accountOwnershipProviders {
|
||||
if !provider.IsChainSupported(chainID) {
|
||||
continue
|
||||
}
|
||||
anyProviderAvailable = true
|
||||
if providerID != thirdparty.FetchFromAnyProvider && providerID != provider.ID() {
|
||||
continue
|
||||
}
|
||||
|
||||
assetContainer, err := provider.FetchAllAssetsByOwner(ctx, chainID, owner, cursor, limit)
|
||||
if err != nil {
|
||||
log.Error("FetchAllAssetsByOwner failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = o.processFullCollectibleData(ctx, assetContainer.Items, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return assetContainer, nil
|
||||
}
|
||||
|
||||
if anyProviderAvailable {
|
||||
return nil, ErrAllProvidersFailedForChainID
|
||||
}
|
||||
return nil, ErrNoProvidersAvailableForChainID
|
||||
}
|
||||
|
||||
func (o *Manager) FetchCollectibleOwnershipByOwner(ctx context.Context, chainID walletCommon.ChainID, owner common.Address, cursor string, limit int, providerID string) (*thirdparty.CollectibleOwnershipContainer, error) {
|
||||
// We don't yet have an API that will return only Ownership data
|
||||
// Use the full Ownership + Metadata endpoint and use the data we need
|
||||
assetContainer, err := o.FetchAllAssetsByOwner(ctx, chainID, owner, cursor, limit, providerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := assetContainer.ToOwnershipContainer()
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
// Returns collectible metadata for the given unique IDs.
|
||||
// If asyncFetch is true, empty metadata will be returned for any missing collectibles and an EventCollectiblesDataUpdated will be sent when the data is ready.
|
||||
// If asyncFetch is false, it will wait for all collectibles' metadata to be retrieved before returning.
|
||||
func (o *Manager) FetchAssetsByCollectibleUniqueID(ctx context.Context, uniqueIDs []thirdparty.CollectibleUniqueID, asyncFetch bool) ([]thirdparty.FullCollectibleData, error) {
|
||||
missingIDs, err := o.collectiblesDataDB.GetIDsNotInDB(uniqueIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
missingIDsPerChainID := thirdparty.GroupCollectibleUIDsByChainID(missingIDs)
|
||||
|
||||
group := async.NewGroup(ctx)
|
||||
group.Add(func(ctx context.Context) error {
|
||||
for chainID, idsToFetch := range missingIDsPerChainID {
|
||||
defer o.checkConnectionStatus(chainID)
|
||||
|
||||
for _, provider := range o.collectibleDataProviders {
|
||||
if !provider.IsChainSupported(chainID) {
|
||||
continue
|
||||
}
|
||||
|
||||
fetchedAssets, err := provider.FetchAssetsByCollectibleUniqueID(ctx, idsToFetch)
|
||||
if err != nil {
|
||||
log.Error("FetchAssetsByCollectibleUniqueID failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
updatedCollectibles, err := o.processFullCollectibleData(ctx, fetchedAssets, asyncFetch)
|
||||
if err != nil {
|
||||
log.Error("processFullCollectibleData failed for", "provider", provider.ID(), "chainID", chainID, "len(fetchedAssets)", len(fetchedAssets), "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if asyncFetch {
|
||||
o.signalUpdatedCollectiblesData(updatedCollectibles)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if !asyncFetch {
|
||||
group.Wait()
|
||||
}
|
||||
|
||||
return o.getCacheFullCollectibleData(uniqueIDs)
|
||||
}
|
||||
|
||||
func (o *Manager) FetchCollectionsDataByContractID(ctx context.Context, ids []thirdparty.ContractID) ([]thirdparty.CollectionData, error) {
|
||||
missingIDs, err := o.collectionsDataDB.GetIDsNotInDB(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
missingIDsPerChainID := thirdparty.GroupContractIDsByChainID(missingIDs)
|
||||
|
||||
for chainID, idsToFetch := range missingIDsPerChainID {
|
||||
defer o.checkConnectionStatus(chainID)
|
||||
|
||||
for _, provider := range o.collectionDataProviders {
|
||||
if !provider.IsChainSupported(chainID) {
|
||||
continue
|
||||
}
|
||||
|
||||
fetchedCollections, err := provider.FetchCollectionsDataByContractID(ctx, idsToFetch)
|
||||
if err != nil {
|
||||
log.Error("FetchCollectionsDataByContractID failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = o.processCollectionData(ctx, fetchedCollections)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
data, err := o.collectionsDataDB.GetData(ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mapToList(data), nil
|
||||
}
|
||||
|
||||
func (o *Manager) getContractOwnershipProviders(chainID walletCommon.ChainID) (mainProvider thirdparty.CollectibleContractOwnershipProvider, fallbackProvider thirdparty.CollectibleContractOwnershipProvider) {
|
||||
mainProvider = nil
|
||||
fallbackProvider = nil
|
||||
|
||||
for _, provider := range o.contractOwnershipProviders {
|
||||
if provider.IsChainSupported(chainID) {
|
||||
if mainProvider == nil {
|
||||
// First provider found
|
||||
mainProvider = provider
|
||||
continue
|
||||
}
|
||||
// Second provider found
|
||||
fallbackProvider = provider
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getCollectibleOwnersByContractAddressFunc(ctx context.Context, chainID walletCommon.ChainID, contractAddress common.Address, provider thirdparty.CollectibleContractOwnershipProvider) func() (any, error) {
|
||||
if provider == nil {
|
||||
return nil
|
||||
}
|
||||
return func() (any, error) {
|
||||
res, err := provider.FetchCollectibleOwnersByContractAddress(ctx, chainID, contractAddress)
|
||||
if err != nil {
|
||||
log.Error("FetchCollectibleOwnersByContractAddress failed for", "provider", provider.ID(), "chainID", chainID, "err", err)
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Manager) FetchCollectibleOwnersByContractAddress(ctx context.Context, chainID walletCommon.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) {
|
||||
defer o.checkConnectionStatus(chainID)
|
||||
|
||||
mainProvider, fallbackProvider := o.getContractOwnershipProviders(chainID)
|
||||
if mainProvider == nil {
|
||||
return nil, ErrNoProvidersAvailableForChainID
|
||||
}
|
||||
|
||||
mainFn := getCollectibleOwnersByContractAddressFunc(ctx, chainID, contractAddress, mainProvider)
|
||||
fallbackFn := getCollectibleOwnersByContractAddressFunc(ctx, chainID, contractAddress, fallbackProvider)
|
||||
|
||||
owners, err := makeContractOwnershipCall(mainFn, fallbackFn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return owners.(*thirdparty.CollectibleContractOwnership), nil
|
||||
}
|
||||
|
||||
func (o *Manager) fetchTokenURI(ctx context.Context, id thirdparty.CollectibleUniqueID) (string, error) {
|
||||
if id.TokenID == nil {
|
||||
return "", errors.New("empty token ID")
|
||||
}
|
||||
backend, err := o.rpcClient.EthClient(uint64(id.ContractID.ChainID))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
caller, err := collectibles.NewCollectiblesCaller(id.ContractID.Address, backend)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tokenURI, err := caller.TokenURI(&bind.CallOpts{
|
||||
Context: ctx,
|
||||
}, id.TokenID.Int)
|
||||
|
||||
if err != nil {
|
||||
for _, errorPrefix := range noTokenURIErrorPrefixes {
|
||||
if strings.HasPrefix(err.Error(), errorPrefix) {
|
||||
// Contract doesn't support "TokenURI" method
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenURI, err
|
||||
}
|
||||
|
||||
func isMetadataEmpty(asset thirdparty.CollectibleData) bool {
|
||||
return asset.Description == "" &&
|
||||
asset.ImageURL == ""
|
||||
}
|
||||
|
||||
// Processes collectible metadata obtained from a provider and ensures any missing data is fetched.
|
||||
// If asyncFetch is true, community collectibles metadata will be fetched async and an EventCollectiblesDataUpdated will be sent when the data is ready.
|
||||
// If asyncFetch is false, it will wait for all community collectibles' metadata to be retrieved before returning.
|
||||
func (o *Manager) processFullCollectibleData(ctx context.Context, assets []thirdparty.FullCollectibleData, asyncFetch bool) ([]thirdparty.CollectibleUniqueID, error) {
|
||||
fullyFetchedAssets := make(map[string]*thirdparty.FullCollectibleData)
|
||||
communityCollectibles := make(map[string][]*thirdparty.FullCollectibleData)
|
||||
processedIDs := make([]thirdparty.CollectibleUniqueID, 0, len(assets))
|
||||
|
||||
// Start with all assets, remove if any of the fetch steps fail
|
||||
for idx := range assets {
|
||||
asset := &assets[idx]
|
||||
id := asset.CollectibleData.ID
|
||||
fullyFetchedAssets[id.HashKey()] = asset
|
||||
}
|
||||
|
||||
// Detect community collectibles
|
||||
for _, asset := range fullyFetchedAssets {
|
||||
// Only check community ownership if metadata is empty
|
||||
if isMetadataEmpty(asset.CollectibleData) {
|
||||
// Get TokenURI if not given by provider
|
||||
err := o.fillTokenURI(ctx, asset)
|
||||
if err != nil {
|
||||
log.Error("fillTokenURI failed", "err", err)
|
||||
delete(fullyFetchedAssets, asset.CollectibleData.ID.HashKey())
|
||||
continue
|
||||
}
|
||||
|
||||
// Get CommunityID if obtainable from TokenURI
|
||||
err = o.fillCommunityID(asset)
|
||||
if err != nil {
|
||||
log.Error("fillCommunityID failed", "err", err)
|
||||
delete(fullyFetchedAssets, asset.CollectibleData.ID.HashKey())
|
||||
continue
|
||||
}
|
||||
|
||||
// Get metadata from community if community collectible
|
||||
communityID := asset.CollectibleData.CommunityID
|
||||
if communityID != "" {
|
||||
if _, ok := communityCollectibles[communityID]; !ok {
|
||||
communityCollectibles[communityID] = make([]*thirdparty.FullCollectibleData, 0)
|
||||
}
|
||||
communityCollectibles[communityID] = append(communityCollectibles[communityID], asset)
|
||||
|
||||
// Community collectibles are handled separately, remove from list
|
||||
delete(fullyFetchedAssets, asset.CollectibleData.ID.HashKey())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Community collectibles are grouped by community ID
|
||||
for communityID, communityAssets := range communityCollectibles {
|
||||
if asyncFetch {
|
||||
o.fetchCommunityAssetsAsync(ctx, communityID, communityAssets)
|
||||
} else {
|
||||
err := o.fetchCommunityAssets(communityID, communityAssets)
|
||||
if err != nil {
|
||||
log.Error("fetchCommunityAssets failed", "communityID", communityID, "err", err)
|
||||
continue
|
||||
}
|
||||
for _, asset := range communityAssets {
|
||||
processedIDs = append(processedIDs, asset.CollectibleData.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, asset := range fullyFetchedAssets {
|
||||
err := o.fillAnimationMediatype(ctx, asset)
|
||||
if err != nil {
|
||||
log.Error("fillAnimationMediatype failed", "err", err)
|
||||
delete(fullyFetchedAssets, asset.CollectibleData.ID.HashKey())
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Save successfully fetched data to DB
|
||||
collectiblesData := make([]thirdparty.CollectibleData, 0, len(assets))
|
||||
collectionsData := make([]thirdparty.CollectionData, 0, len(assets))
|
||||
missingCollectionIDs := make([]thirdparty.ContractID, 0)
|
||||
|
||||
for _, asset := range fullyFetchedAssets {
|
||||
id := asset.CollectibleData.ID
|
||||
processedIDs = append(processedIDs, id)
|
||||
|
||||
collectiblesData = append(collectiblesData, asset.CollectibleData)
|
||||
if asset.CollectionData != nil {
|
||||
collectionsData = append(collectionsData, *asset.CollectionData)
|
||||
} else {
|
||||
missingCollectionIDs = append(missingCollectionIDs, id.ContractID)
|
||||
}
|
||||
}
|
||||
|
||||
err := o.collectiblesDataDB.SetData(collectiblesData, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = o.collectionsDataDB.SetData(collectionsData, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(missingCollectionIDs) > 0 {
|
||||
// Calling this ensures collection data is fetched and cached (if not already available)
|
||||
_, err := o.FetchCollectionsDataByContractID(ctx, missingCollectionIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return processedIDs, nil
|
||||
}
|
||||
|
||||
func (o *Manager) fillTokenURI(ctx context.Context, asset *thirdparty.FullCollectibleData) error {
|
||||
id := asset.CollectibleData.ID
|
||||
|
||||
tokenURI := asset.CollectibleData.TokenURI
|
||||
// Only need to fetch it from contract if it was empty
|
||||
if tokenURI == "" {
|
||||
tokenURI, err := o.fetchTokenURI(ctx, id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
asset.CollectibleData.TokenURI = tokenURI
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Manager) fillCommunityID(asset *thirdparty.FullCollectibleData) error {
|
||||
tokenURI := asset.CollectibleData.TokenURI
|
||||
|
||||
communityID := ""
|
||||
if tokenURI != "" {
|
||||
communityID = o.communityManager.GetCommunityID(tokenURI)
|
||||
}
|
||||
|
||||
asset.CollectibleData.CommunityID = communityID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Manager) fetchCommunityAssets(communityID string, communityAssets []*thirdparty.FullCollectibleData) error {
|
||||
communityInfo, err := o.communityManager.FetchCommunityInfo(communityID)
|
||||
|
||||
// If the community is found, we update the DB.
|
||||
// If the community is not found, we only insert new entries to the DB (don't replace what is already there).
|
||||
allowUpdate := false
|
||||
if err != nil {
|
||||
log.Error("fetchCommunityInfo failed", "communityID", communityID, "err", err)
|
||||
} else if communityInfo == nil {
|
||||
log.Warn("fetchCommunityAssets community not found", "communityID", communityID)
|
||||
} else {
|
||||
for _, communityAsset := range communityAssets {
|
||||
err := o.communityManager.FillCollectibleMetadata(communityAsset)
|
||||
if err != nil {
|
||||
log.Error("FillCollectibleMetadata failed", "communityID", communityID, "err", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
allowUpdate = true
|
||||
}
|
||||
|
||||
collectiblesData := make([]thirdparty.CollectibleData, 0, len(communityAssets))
|
||||
collectionsData := make([]thirdparty.CollectionData, 0, len(communityAssets))
|
||||
|
||||
for _, asset := range communityAssets {
|
||||
collectiblesData = append(collectiblesData, asset.CollectibleData)
|
||||
if asset.CollectionData != nil {
|
||||
collectionsData = append(collectionsData, *asset.CollectionData)
|
||||
}
|
||||
}
|
||||
|
||||
err = o.collectiblesDataDB.SetData(collectiblesData, allowUpdate)
|
||||
if err != nil {
|
||||
log.Error("collectiblesDataDB SetData failed", "communityID", communityID, "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = o.collectionsDataDB.SetData(collectionsData, allowUpdate)
|
||||
if err != nil {
|
||||
log.Error("collectionsDataDB SetData failed", "communityID", communityID, "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, asset := range communityAssets {
|
||||
if asset.CollectibleCommunityInfo != nil {
|
||||
err = o.collectiblesDataDB.SetCommunityInfo(asset.CollectibleData.ID, *asset.CollectibleCommunityInfo)
|
||||
if err != nil {
|
||||
log.Error("collectiblesDataDB SetCommunityInfo failed", "communityID", communityID, "err", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Manager) fetchCommunityAssetsAsync(ctx context.Context, communityID string, communityAssets []*thirdparty.FullCollectibleData) {
|
||||
if len(communityAssets) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := o.fetchCommunityAssets(communityID, communityAssets)
|
||||
if err != nil {
|
||||
log.Error("fetchCommunityAssets failed", "communityID", communityID, "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Metadata is up to date in db at this point, fetch and send Event.
|
||||
ids := make([]thirdparty.CollectibleUniqueID, 0, len(communityAssets))
|
||||
for _, asset := range communityAssets {
|
||||
ids = append(ids, asset.CollectibleData.ID)
|
||||
}
|
||||
o.signalUpdatedCollectiblesData(ids)
|
||||
}()
|
||||
}
|
||||
|
||||
func (o *Manager) fillAnimationMediatype(ctx context.Context, asset *thirdparty.FullCollectibleData) error {
|
||||
if len(asset.CollectibleData.AnimationURL) > 0 {
|
||||
contentType, err := o.doContentTypeRequest(ctx, asset.CollectibleData.AnimationURL)
|
||||
if err != nil {
|
||||
asset.CollectibleData.AnimationURL = ""
|
||||
}
|
||||
asset.CollectibleData.AnimationMediaType = contentType
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Manager) processCollectionData(ctx context.Context, collections []thirdparty.CollectionData) error {
|
||||
return o.collectionsDataDB.SetData(collections, true)
|
||||
}
|
||||
|
||||
func (o *Manager) getCacheFullCollectibleData(uniqueIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.FullCollectibleData, error) {
|
||||
ret := make([]thirdparty.FullCollectibleData, 0, len(uniqueIDs))
|
||||
|
||||
collectiblesData, err := o.collectiblesDataDB.GetData(uniqueIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contractIDs := make([]thirdparty.ContractID, 0, len(uniqueIDs))
|
||||
for _, id := range uniqueIDs {
|
||||
contractIDs = append(contractIDs, id.ContractID)
|
||||
}
|
||||
|
||||
collectionsData, err := o.collectionsDataDB.GetData(contractIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, id := range uniqueIDs {
|
||||
collectibleData, ok := collectiblesData[id.HashKey()]
|
||||
if !ok {
|
||||
// Use empty data, set only ID
|
||||
collectibleData = thirdparty.CollectibleData{
|
||||
ID: id,
|
||||
}
|
||||
}
|
||||
if o.mediaServer != nil && len(collectibleData.ImagePayload) > 0 {
|
||||
collectibleData.ImageURL = o.mediaServer.MakeWalletCollectibleImagesURL(collectibleData.ID)
|
||||
}
|
||||
|
||||
collectionData, ok := collectionsData[id.ContractID.HashKey()]
|
||||
if !ok {
|
||||
// Use empty data, set only ID
|
||||
collectionData = thirdparty.CollectionData{
|
||||
ID: id.ContractID,
|
||||
}
|
||||
}
|
||||
if o.mediaServer != nil && len(collectionData.ImagePayload) > 0 {
|
||||
collectionData.ImageURL = o.mediaServer.MakeWalletCollectionImagesURL(collectionData.ID)
|
||||
}
|
||||
|
||||
communityInfo, _, err := o.communityManager.GetCommunityInfo(collectibleData.CommunityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
collectibleCommunityInfo, err := o.collectiblesDataDB.GetCommunityInfo(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ownership, err := o.ownershipDB.GetOwnership(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fullData := thirdparty.FullCollectibleData{
|
||||
CollectibleData: collectibleData,
|
||||
CollectionData: &collectionData,
|
||||
CommunityInfo: communityInfo,
|
||||
CollectibleCommunityInfo: collectibleCommunityInfo,
|
||||
Ownership: ownership,
|
||||
}
|
||||
ret = append(ret, fullData)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (o *Manager) SetCollectibleTransferID(ownerAddress common.Address, id thirdparty.CollectibleUniqueID, transferID common.Hash, notify bool) error {
|
||||
changed, err := o.ownershipDB.SetTransferID(ownerAddress, id, transferID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if changed && notify {
|
||||
o.signalUpdatedCollectiblesData([]thirdparty.CollectibleUniqueID{id})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset connection status to trigger notifications
|
||||
// on the next status update
|
||||
func (o *Manager) ResetConnectionStatus() {
|
||||
for _, status := range o.statuses {
|
||||
status.ResetStateValue()
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Manager) checkConnectionStatus(chainID walletCommon.ChainID) {
|
||||
for _, provider := range o.collectibleProviders {
|
||||
if provider.IsChainSupported(chainID) && provider.IsConnected() {
|
||||
o.statuses[chainID.String()].SetIsConnected(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
o.statuses[chainID.String()].SetIsConnected(false)
|
||||
}
|
||||
|
||||
func (o *Manager) signalUpdatedCollectiblesData(ids []thirdparty.CollectibleUniqueID) {
|
||||
// We limit how much collectibles data we send in each event to avoid problems on the client side
|
||||
for startIdx := 0; startIdx < len(ids); startIdx += signalUpdatedCollectiblesDataPageSize {
|
||||
endIdx := startIdx + signalUpdatedCollectiblesDataPageSize
|
||||
if endIdx > len(ids) {
|
||||
endIdx = len(ids)
|
||||
}
|
||||
pageIDs := ids[startIdx:endIdx]
|
||||
|
||||
collectibles, err := o.getCacheFullCollectibleData(pageIDs)
|
||||
if err != nil {
|
||||
log.Error("Error getting FullCollectibleData from cache: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Send update event with most complete data type available
|
||||
details := fullCollectiblesDataToDetails(collectibles)
|
||||
|
||||
payload, err := json.Marshal(details)
|
||||
if err != nil {
|
||||
log.Error("Error marshaling response: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
event := walletevent.Event{
|
||||
Type: EventCollectiblesDataUpdated,
|
||||
Message: string(payload),
|
||||
}
|
||||
|
||||
o.feed.Send(event)
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+612
@@ -0,0 +1,612 @@
|
||||
package collectibles
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
w_common "github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
"github.com/status-im/status-go/sqlite"
|
||||
)
|
||||
|
||||
const InvalidTimestamp = int64(-1)
|
||||
|
||||
type OwnershipDB struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewOwnershipDB(sqlDb *sql.DB) *OwnershipDB {
|
||||
return &OwnershipDB{
|
||||
db: sqlDb,
|
||||
}
|
||||
}
|
||||
|
||||
const unknownUpdateTimestamp = int64(math.MaxInt64)
|
||||
|
||||
const selectOwnershipColumns = "chain_id, contract_address, token_id"
|
||||
|
||||
const ownershipTimestampColumns = "owner_address, chain_id, timestamp"
|
||||
const selectOwnershipTimestampColumns = "timestamp"
|
||||
|
||||
func insertTmpOwnership(
|
||||
db *sql.DB,
|
||||
chainID w_common.ChainID,
|
||||
ownerAddress common.Address,
|
||||
balancesPerContractAdddress thirdparty.TokenBalancesPerContractAddress,
|
||||
) error {
|
||||
// Put old/new ownership data into temp tables
|
||||
// NOTE: Temp table CREATE doesn't work with prepared statements,
|
||||
// so we have to use Exec directly
|
||||
_, err := db.Exec(`
|
||||
DROP TABLE IF EXISTS temp.old_collectibles_ownership_cache;
|
||||
CREATE TABLE temp.old_collectibles_ownership_cache(
|
||||
contract_address VARCHAR NOT NULL,
|
||||
token_id BLOB NOT NULL,
|
||||
balance BLOB NOT NULL
|
||||
);
|
||||
DROP TABLE IF EXISTS temp.new_collectibles_ownership_cache;
|
||||
CREATE TABLE temp.new_collectibles_ownership_cache(
|
||||
contract_address VARCHAR NOT NULL,
|
||||
token_id BLOB NOT NULL,
|
||||
balance BLOB NOT NULL
|
||||
);`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
insertTmpOldOwnership, err := db.Prepare(`
|
||||
INSERT INTO temp.old_collectibles_ownership_cache
|
||||
SELECT contract_address, token_id, balance FROM collectibles_ownership_cache
|
||||
WHERE chain_id = ? AND owner_address = ?`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer insertTmpOldOwnership.Close()
|
||||
|
||||
_, err = insertTmpOldOwnership.Exec(chainID, ownerAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
insertTmpNewOwnership, err := db.Prepare(`
|
||||
INSERT INTO temp.new_collectibles_ownership_cache (contract_address, token_id, balance)
|
||||
VALUES (?, ?, ?)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer insertTmpNewOwnership.Close()
|
||||
|
||||
for contractAddress, balances := range balancesPerContractAdddress {
|
||||
for _, balance := range balances {
|
||||
_, err = insertTmpNewOwnership.Exec(
|
||||
contractAddress,
|
||||
(*bigint.SQLBigIntBytes)(balance.TokenID.Int),
|
||||
(*bigint.SQLBigIntBytes)(balance.Balance.Int),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeOldAddressOwnership(
|
||||
creator sqlite.StatementCreator,
|
||||
chainID w_common.ChainID,
|
||||
ownerAddress common.Address,
|
||||
) ([]thirdparty.CollectibleUniqueID, error) {
|
||||
// Find collectibles in the DB that are not in the temp table
|
||||
removedQuery, err := creator.Prepare(fmt.Sprintf(`
|
||||
SELECT %d, tOld.contract_address, tOld.token_id
|
||||
FROM temp.old_collectibles_ownership_cache tOld
|
||||
LEFT JOIN temp.new_collectibles_ownership_cache tNew ON
|
||||
tOld.contract_address = tNew.contract_address AND tOld.token_id = tNew.token_id
|
||||
WHERE
|
||||
tNew.contract_address IS NULL
|
||||
`, chainID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer removedQuery.Close()
|
||||
|
||||
removedRows, err := removedQuery.Query()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer removedRows.Close()
|
||||
removedIDs, err := thirdparty.RowsToCollectibles(removedRows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
removeOwnership, err := creator.Prepare("DELETE FROM collectibles_ownership_cache WHERE chain_id = ? AND owner_address = ? AND contract_address = ? AND token_id = ?")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer removeOwnership.Close()
|
||||
|
||||
for _, id := range removedIDs {
|
||||
_, err = removeOwnership.Exec(
|
||||
chainID,
|
||||
ownerAddress,
|
||||
id.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(id.TokenID.Int),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return removedIDs, nil
|
||||
}
|
||||
|
||||
func updateChangedAddressOwnership(
|
||||
creator sqlite.StatementCreator,
|
||||
chainID w_common.ChainID,
|
||||
ownerAddress common.Address,
|
||||
) ([]thirdparty.CollectibleUniqueID, error) {
|
||||
// Find collectibles in the temp table that are in the DB and have a different balance
|
||||
updatedQuery, err := creator.Prepare(fmt.Sprintf(`
|
||||
SELECT %d, tNew.contract_address, tNew.token_id
|
||||
FROM temp.new_collectibles_ownership_cache tNew
|
||||
LEFT JOIN temp.old_collectibles_ownership_cache tOld ON
|
||||
tOld.contract_address = tNew.contract_address AND tOld.token_id = tNew.token_id
|
||||
WHERE
|
||||
tOld.contract_address IS NOT NULL AND tOld.balance != tNew.balance
|
||||
`, chainID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer updatedQuery.Close()
|
||||
|
||||
updatedRows, err := updatedQuery.Query()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer updatedRows.Close()
|
||||
|
||||
updatedIDs, err := thirdparty.RowsToCollectibles(updatedRows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updateOwnership, err := creator.Prepare(`
|
||||
UPDATE collectibles_ownership_cache
|
||||
SET balance = (SELECT tNew.balance
|
||||
FROM temp.new_collectibles_ownership_cache tNew
|
||||
WHERE tNew.contract_address = collectibles_ownership_cache.contract_address AND tNew.token_id = collectibles_ownership_cache.token_id)
|
||||
WHERE chain_id = ? AND owner_address = ? AND contract_address = ? AND token_id = ?
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer updateOwnership.Close()
|
||||
|
||||
for _, id := range updatedIDs {
|
||||
_, err = updateOwnership.Exec(
|
||||
chainID,
|
||||
ownerAddress,
|
||||
id.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(id.TokenID.Int))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return updatedIDs, nil
|
||||
}
|
||||
|
||||
func insertNewAddressOwnership(
|
||||
creator sqlite.StatementCreator,
|
||||
chainID w_common.ChainID,
|
||||
ownerAddress common.Address,
|
||||
) ([]thirdparty.CollectibleUniqueID, error) {
|
||||
// Find collectibles in the temp table that are not in the DB
|
||||
insertedQuery, err := creator.Prepare(fmt.Sprintf(`
|
||||
SELECT %d, tNew.contract_address, tNew.token_id
|
||||
FROM temp.new_collectibles_ownership_cache tNew
|
||||
LEFT JOIN temp.old_collectibles_ownership_cache tOld ON
|
||||
tOld.contract_address = tNew.contract_address AND tOld.token_id = tNew.token_id
|
||||
WHERE
|
||||
tOld.contract_address IS NULL
|
||||
`, chainID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer insertedQuery.Close()
|
||||
|
||||
insertedRows, err := insertedQuery.Query()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer insertedRows.Close()
|
||||
|
||||
insertedIDs, err := thirdparty.RowsToCollectibles(insertedRows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
insertOwnership, err := creator.Prepare(fmt.Sprintf(`
|
||||
INSERT INTO collectibles_ownership_cache
|
||||
SELECT
|
||||
%d, tNew.contract_address, tNew.token_id, X'%s', tNew.balance, NULL
|
||||
FROM temp.new_collectibles_ownership_cache tNew
|
||||
WHERE
|
||||
tNew.contract_address = ? AND tNew.token_id = ?
|
||||
`, chainID, ownerAddress.Hex()[2:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer insertOwnership.Close()
|
||||
|
||||
for _, id := range insertedIDs {
|
||||
_, err = insertOwnership.Exec(
|
||||
id.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(id.TokenID.Int))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return insertedIDs, nil
|
||||
}
|
||||
|
||||
func updateAddressOwnership(
|
||||
tx sqlite.StatementCreator,
|
||||
chainID w_common.ChainID,
|
||||
ownerAddress common.Address,
|
||||
) (removedIDs, updatedIDs, insertedIDs []thirdparty.CollectibleUniqueID, err error) {
|
||||
removedIDs, err = removeOldAddressOwnership(tx, chainID, ownerAddress)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
updatedIDs, err = updateChangedAddressOwnership(tx, chainID, ownerAddress)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
insertedIDs, err = insertNewAddressOwnership(tx, chainID, ownerAddress)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func updateAddressOwnershipTimestamp(creator sqlite.StatementCreator, ownerAddress common.Address, chainID w_common.ChainID, timestamp int64) error {
|
||||
updateTimestamp, err := creator.Prepare(fmt.Sprintf(`INSERT OR REPLACE INTO collectibles_ownership_update_timestamps (%s)
|
||||
VALUES (?, ?, ?)`, ownershipTimestampColumns))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer updateTimestamp.Close()
|
||||
|
||||
_, err = updateTimestamp.Exec(ownerAddress, chainID, timestamp)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Returns the list of added/removed IDs when comparing the given list of IDs with the ones in the DB.
|
||||
// Call before Update for the result to be useful.
|
||||
func (o *OwnershipDB) GetIDsNotInDB(
|
||||
chainID w_common.ChainID,
|
||||
ownerAddress common.Address,
|
||||
newIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleUniqueID, error) {
|
||||
ret := make([]thirdparty.CollectibleUniqueID, 0, len(newIDs))
|
||||
|
||||
exists, err := o.db.Prepare(`SELECT EXISTS (
|
||||
SELECT 1 FROM collectibles_ownership_cache
|
||||
WHERE chain_id=? AND contract_address=? AND token_id=? AND owner_address=?
|
||||
)`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, id := range newIDs {
|
||||
row := exists.QueryRow(
|
||||
id.ContractID.ChainID,
|
||||
id.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(id.TokenID.Int),
|
||||
ownerAddress,
|
||||
)
|
||||
var exists bool
|
||||
err = row.Scan(&exists)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
ret = append(ret, id)
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (o *OwnershipDB) Update(chainID w_common.ChainID, ownerAddress common.Address, balances thirdparty.TokenBalancesPerContractAddress, timestamp int64) (removedIDs, updatedIDs, insertedIDs []thirdparty.CollectibleUniqueID, err error) {
|
||||
err = insertTmpOwnership(o.db, chainID, ownerAddress, balances)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
tx *sql.Tx
|
||||
)
|
||||
tx, err = o.db.Begin()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err == nil {
|
||||
err = tx.Commit()
|
||||
return
|
||||
}
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
// Compare tmp and current ownership tables and update the current one
|
||||
removedIDs, updatedIDs, insertedIDs, err = updateAddressOwnership(tx, chainID, ownerAddress)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Update timestamp
|
||||
err = updateAddressOwnershipTimestamp(tx, ownerAddress, chainID, timestamp)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (o *OwnershipDB) GetOwnedCollectibles(chainIDs []w_common.ChainID, ownerAddresses []common.Address, offset int, limit int) ([]thirdparty.CollectibleUniqueID, error) {
|
||||
query, args, err := sqlx.In(fmt.Sprintf(`SELECT %s
|
||||
FROM collectibles_ownership_cache
|
||||
WHERE chain_id IN (?) AND owner_address IN (?)
|
||||
LIMIT ? OFFSET ?`, selectOwnershipColumns), chainIDs, ownerAddresses, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stmt, err := o.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
rows, err := stmt.Query(args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return thirdparty.RowsToCollectibles(rows)
|
||||
}
|
||||
|
||||
func (o *OwnershipDB) GetOwnedCollectible(chainID w_common.ChainID, ownerAddresses common.Address, contractAddress common.Address, tokenID *big.Int) (*thirdparty.CollectibleUniqueID, error) {
|
||||
query := fmt.Sprintf(`SELECT %s
|
||||
FROM collectibles_ownership_cache
|
||||
WHERE chain_id = ? AND owner_address = ? AND contract_address = ? AND token_id = ?`, selectOwnershipColumns)
|
||||
|
||||
stmt, err := o.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
rows, err := stmt.Query(chainID, ownerAddresses, contractAddress, (*bigint.SQLBigIntBytes)(tokenID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
ids, err := thirdparty.RowsToCollectibles(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &ids[0], nil
|
||||
}
|
||||
|
||||
func (o *OwnershipDB) GetOwnershipUpdateTimestamp(owner common.Address, chainID w_common.ChainID) (int64, error) {
|
||||
query := fmt.Sprintf(`SELECT %s
|
||||
FROM collectibles_ownership_update_timestamps
|
||||
WHERE owner_address = ? AND chain_id = ?`, selectOwnershipTimestampColumns)
|
||||
|
||||
stmt, err := o.db.Prepare(query)
|
||||
if err != nil {
|
||||
return InvalidTimestamp, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
row := stmt.QueryRow(owner, chainID)
|
||||
|
||||
var timestamp int64
|
||||
|
||||
err = row.Scan(×tamp)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return InvalidTimestamp, nil
|
||||
} else if err != nil {
|
||||
return InvalidTimestamp, err
|
||||
}
|
||||
|
||||
return timestamp, nil
|
||||
}
|
||||
|
||||
func (o *OwnershipDB) GetLatestOwnershipUpdateTimestamp(chainID w_common.ChainID) (int64, error) {
|
||||
query := `SELECT MAX(timestamp)
|
||||
FROM collectibles_ownership_update_timestamps
|
||||
WHERE chain_id = ?`
|
||||
|
||||
stmt, err := o.db.Prepare(query)
|
||||
if err != nil {
|
||||
return InvalidTimestamp, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
row := stmt.QueryRow(chainID)
|
||||
|
||||
var timestamp sql.NullInt64
|
||||
|
||||
err = row.Scan(×tamp)
|
||||
|
||||
if err != nil {
|
||||
return InvalidTimestamp, err
|
||||
}
|
||||
if timestamp.Valid {
|
||||
return timestamp.Int64, nil
|
||||
}
|
||||
|
||||
return InvalidTimestamp, nil
|
||||
}
|
||||
|
||||
func (o *OwnershipDB) GetOwnership(id thirdparty.CollectibleUniqueID) ([]thirdparty.AccountBalance, error) {
|
||||
query := fmt.Sprintf(`SELECT c.owner_address, c.balance, COALESCE(t.timestamp, %d)
|
||||
FROM collectibles_ownership_cache c
|
||||
LEFT JOIN transfers t ON
|
||||
c.transfer_id = t.hash
|
||||
WHERE
|
||||
c.chain_id = ? AND c.contract_address = ? AND c.token_id = ?`, unknownUpdateTimestamp)
|
||||
|
||||
stmt, err := o.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
rows, err := stmt.Query(id.ContractID.ChainID, id.ContractID.Address, (*bigint.SQLBigIntBytes)(id.TokenID.Int))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var ret []thirdparty.AccountBalance
|
||||
for rows.Next() {
|
||||
accountBalance := thirdparty.AccountBalance{
|
||||
Balance: &bigint.BigInt{Int: big.NewInt(0)},
|
||||
}
|
||||
err = rows.Scan(
|
||||
&accountBalance.Address,
|
||||
(*bigint.SQLBigIntBytes)(accountBalance.Balance.Int),
|
||||
&accountBalance.TxTimestamp,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret = append(ret, accountBalance)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (o *OwnershipDB) SetTransferID(ownerAddress common.Address, id thirdparty.CollectibleUniqueID, transferID common.Hash) (bool, error) {
|
||||
query := `UPDATE collectibles_ownership_cache
|
||||
SET transfer_id = ?
|
||||
WHERE chain_id = ? AND contract_address = ? AND token_id = ? AND owner_address = ?`
|
||||
|
||||
stmt, err := o.db.Prepare(query)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
res, err := stmt.Exec(transferID, id.ContractID.ChainID, id.ContractID.Address, (*bigint.SQLBigIntBytes)(id.TokenID.Int), ownerAddress)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if rowsAffected > 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (o *OwnershipDB) GetTransferID(ownerAddress common.Address, id thirdparty.CollectibleUniqueID) (*common.Hash, error) {
|
||||
query := `SELECT transfer_id
|
||||
FROM collectibles_ownership_cache
|
||||
WHERE chain_id = ? AND contract_address = ? AND token_id = ? AND owner_address = ?
|
||||
LIMIT 1`
|
||||
|
||||
stmt, err := o.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
row := stmt.QueryRow(id.ContractID.ChainID, id.ContractID.Address, (*bigint.SQLBigIntBytes)(id.TokenID.Int), ownerAddress)
|
||||
|
||||
var dbTransferID []byte
|
||||
|
||||
err = row.Scan(&dbTransferID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(dbTransferID) > 0 {
|
||||
transferID := common.BytesToHash(dbTransferID)
|
||||
return &transferID, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (o *OwnershipDB) GetCollectiblesWithNoTransferID(account common.Address, chainID w_common.ChainID) ([]thirdparty.CollectibleUniqueID, error) {
|
||||
query := `SELECT contract_address, token_id
|
||||
FROM collectibles_ownership_cache
|
||||
WHERE chain_id = ? AND owner_address = ? AND transfer_id IS NULL`
|
||||
|
||||
stmt, err := o.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
rows, err := stmt.Query(chainID, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var ret []thirdparty.CollectibleUniqueID
|
||||
for rows.Next() {
|
||||
id := thirdparty.CollectibleUniqueID{
|
||||
ContractID: thirdparty.ContractID{
|
||||
ChainID: chainID,
|
||||
},
|
||||
TokenID: &bigint.BigInt{Int: big.NewInt(0)},
|
||||
}
|
||||
err = rows.Scan(
|
||||
&id.ContractID.Address,
|
||||
(*bigint.SQLBigIntBytes)(id.TokenID.Int),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret = append(ret, id)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
+491
@@ -0,0 +1,491 @@
|
||||
package collectibles
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/event"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
|
||||
"github.com/status-im/status-go/multiaccounts/accounts"
|
||||
"github.com/status-im/status-go/rpc/network"
|
||||
|
||||
"github.com/status-im/status-go/services/wallet/async"
|
||||
"github.com/status-im/status-go/services/wallet/bigint"
|
||||
walletCommon "github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/community"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
"github.com/status-im/status-go/services/wallet/transfer"
|
||||
"github.com/status-im/status-go/services/wallet/walletevent"
|
||||
)
|
||||
|
||||
// These events are used to notify the UI of state changes
|
||||
const (
|
||||
EventCollectiblesOwnershipUpdateStarted walletevent.EventType = "wallet-collectibles-ownership-update-started"
|
||||
EventCollectiblesOwnershipUpdatePartial walletevent.EventType = "wallet-collectibles-ownership-update-partial"
|
||||
EventCollectiblesOwnershipUpdateFinished walletevent.EventType = "wallet-collectibles-ownership-update-finished"
|
||||
EventCollectiblesOwnershipUpdateFinishedWithError walletevent.EventType = "wallet-collectibles-ownership-update-finished-with-error"
|
||||
EventCommunityCollectiblesReceived walletevent.EventType = "wallet-collectibles-community-collectibles-received"
|
||||
EventCollectiblesDataUpdated walletevent.EventType = "wallet-collectibles-data-updated"
|
||||
|
||||
EventOwnedCollectiblesFilteringDone walletevent.EventType = "wallet-owned-collectibles-filtering-done"
|
||||
EventGetCollectiblesDetailsDone walletevent.EventType = "wallet-get-collectibles-details-done"
|
||||
)
|
||||
|
||||
type OwnershipUpdateMessage struct {
|
||||
Added []thirdparty.CollectibleUniqueID `json:"added"`
|
||||
Updated []thirdparty.CollectibleUniqueID `json:"updated"`
|
||||
Removed []thirdparty.CollectibleUniqueID `json:"removed"`
|
||||
}
|
||||
|
||||
type CollectibleDataType byte
|
||||
|
||||
const (
|
||||
CollectibleDataTypeUniqueID CollectibleDataType = iota
|
||||
CollectibleDataTypeHeader
|
||||
CollectibleDataTypeDetails
|
||||
CollectibleDataTypeCommunityHeader
|
||||
)
|
||||
|
||||
type FetchType byte
|
||||
|
||||
const (
|
||||
FetchTypeNeverFetch FetchType = iota
|
||||
FetchTypeAlwaysFetch
|
||||
FetchTypeFetchIfNotCached
|
||||
FetchTypeFetchIfCacheOld
|
||||
)
|
||||
|
||||
type FetchCriteria struct {
|
||||
FetchType FetchType `json:"fetch_type"`
|
||||
MaxCacheAgeSeconds int64 `json:"max_cache_age_seconds"`
|
||||
}
|
||||
|
||||
var (
|
||||
filterOwnedCollectiblesTask = async.TaskType{
|
||||
ID: 1,
|
||||
Policy: async.ReplacementPolicyCancelOld,
|
||||
}
|
||||
getCollectiblesDataTask = async.TaskType{
|
||||
ID: 2,
|
||||
Policy: async.ReplacementPolicyCancelOld,
|
||||
}
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
manager *Manager
|
||||
controller *Controller
|
||||
db *sql.DB
|
||||
ownershipDB *OwnershipDB
|
||||
transferDB *transfer.Database
|
||||
communityManager *community.Manager
|
||||
walletFeed *event.Feed
|
||||
scheduler *async.MultiClientScheduler
|
||||
group *async.Group
|
||||
}
|
||||
|
||||
func NewService(
|
||||
db *sql.DB,
|
||||
walletFeed *event.Feed,
|
||||
accountsDB *accounts.Database,
|
||||
accountsFeed *event.Feed,
|
||||
settingsFeed *event.Feed,
|
||||
communityManager *community.Manager,
|
||||
networkManager *network.Manager,
|
||||
manager *Manager) *Service {
|
||||
s := &Service{
|
||||
manager: manager,
|
||||
controller: NewController(db, walletFeed, accountsDB, accountsFeed, settingsFeed, networkManager, manager),
|
||||
db: db,
|
||||
ownershipDB: NewOwnershipDB(db),
|
||||
transferDB: transfer.NewDB(db),
|
||||
communityManager: communityManager,
|
||||
walletFeed: walletFeed,
|
||||
scheduler: async.NewMultiClientScheduler(),
|
||||
group: async.NewGroup(context.Background()),
|
||||
}
|
||||
s.controller.SetOwnedCollectiblesChangeCb(s.onOwnedCollectiblesChange)
|
||||
s.controller.SetCollectiblesTransferCb(s.onCollectiblesTransfer)
|
||||
return s
|
||||
}
|
||||
|
||||
type ErrorCode = int
|
||||
|
||||
const (
|
||||
ErrorCodeSuccess ErrorCode = iota + 1
|
||||
ErrorCodeTaskCanceled
|
||||
ErrorCodeFailed
|
||||
)
|
||||
|
||||
type OwnershipStatus struct {
|
||||
State OwnershipState `json:"state"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type OwnershipStatusPerChainID = map[walletCommon.ChainID]OwnershipStatus
|
||||
type OwnershipStatusPerAddressAndChainID = map[common.Address]OwnershipStatusPerChainID
|
||||
|
||||
type GetOwnedCollectiblesResponse struct {
|
||||
Collectibles []Collectible `json:"collectibles"`
|
||||
Offset int `json:"offset"`
|
||||
// Used to indicate that there might be more collectibles that were not returned
|
||||
// based on a simple heuristic
|
||||
HasMore bool `json:"hasMore"`
|
||||
OwnershipStatus OwnershipStatusPerAddressAndChainID `json:"ownershipStatus"`
|
||||
ErrorCode ErrorCode `json:"errorCode"`
|
||||
}
|
||||
|
||||
type GetCollectiblesByUniqueIDResponse struct {
|
||||
Collectibles []Collectible `json:"collectibles"`
|
||||
ErrorCode ErrorCode `json:"errorCode"`
|
||||
}
|
||||
|
||||
type GetOwnedCollectiblesReturnType struct {
|
||||
collectibles []Collectible
|
||||
hasMore bool
|
||||
ownershipStatus OwnershipStatusPerAddressAndChainID
|
||||
}
|
||||
|
||||
type GetCollectiblesByUniqueIDReturnType struct {
|
||||
collectibles []Collectible
|
||||
}
|
||||
|
||||
func (s *Service) GetOwnedCollectibles(
|
||||
ctx context.Context,
|
||||
chainIDs []walletCommon.ChainID,
|
||||
addresses []common.Address,
|
||||
filter Filter,
|
||||
offset int,
|
||||
limit int,
|
||||
dataType CollectibleDataType,
|
||||
fetchCriteria FetchCriteria) (*GetOwnedCollectiblesReturnType, error) {
|
||||
err := s.fetchOwnedCollectiblesIfNeeded(ctx, chainIDs, addresses, fetchCriteria)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ids, hasMore, err := s.FilterOwnedCollectibles(chainIDs, addresses, filter, offset, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
collectibles, err := s.collectibleIDsToDataType(ctx, ids, dataType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ownershipStatus, err := s.GetOwnershipStatus(chainIDs, addresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &GetOwnedCollectiblesReturnType{
|
||||
collectibles: collectibles,
|
||||
hasMore: hasMore,
|
||||
ownershipStatus: ownershipStatus,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (s *Service) needsToFetch(chainID walletCommon.ChainID, address common.Address, fetchCriteria FetchCriteria) (bool, error) {
|
||||
mustFetch := false
|
||||
switch fetchCriteria.FetchType {
|
||||
case FetchTypeAlwaysFetch:
|
||||
mustFetch = true
|
||||
case FetchTypeNeverFetch:
|
||||
mustFetch = false
|
||||
case FetchTypeFetchIfNotCached, FetchTypeFetchIfCacheOld:
|
||||
timestamp, err := s.ownershipDB.GetOwnershipUpdateTimestamp(address, chainID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if timestamp == InvalidTimestamp ||
|
||||
(fetchCriteria.FetchType == FetchTypeFetchIfCacheOld && timestamp+fetchCriteria.MaxCacheAgeSeconds < time.Now().Unix()) {
|
||||
mustFetch = true
|
||||
}
|
||||
}
|
||||
return mustFetch, nil
|
||||
}
|
||||
|
||||
func (s *Service) fetchOwnedCollectiblesIfNeeded(ctx context.Context, chainIDs []walletCommon.ChainID, addresses []common.Address, fetchCriteria FetchCriteria) error {
|
||||
if fetchCriteria.FetchType == FetchTypeNeverFetch {
|
||||
return nil
|
||||
}
|
||||
|
||||
group := async.NewGroup(ctx)
|
||||
for _, address := range addresses {
|
||||
for _, chainID := range chainIDs {
|
||||
mustFetch, err := s.needsToFetch(chainID, address, fetchCriteria)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if mustFetch {
|
||||
command := newLoadOwnedCollectiblesCommand(s.manager, s.ownershipDB, s.walletFeed, chainID, address, nil)
|
||||
group.Add(command.Command())
|
||||
}
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-group.WaitAsync():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetOwnedCollectiblesAsync allows only one filter task to run at a time
|
||||
// and it cancels the current one if a new one is started
|
||||
// All calls will trigger an EventOwnedCollectiblesFilteringDone event with the result of the filtering
|
||||
func (s *Service) GetOwnedCollectiblesAsync(
|
||||
requestID int32,
|
||||
chainIDs []walletCommon.ChainID,
|
||||
addresses []common.Address,
|
||||
filter Filter,
|
||||
offset int,
|
||||
limit int,
|
||||
dataType CollectibleDataType,
|
||||
fetchCriteria FetchCriteria) {
|
||||
s.scheduler.Enqueue(requestID, filterOwnedCollectiblesTask, func(ctx context.Context) (interface{}, error) {
|
||||
return s.GetOwnedCollectibles(ctx, chainIDs, addresses, filter, offset, limit, dataType, fetchCriteria)
|
||||
}, func(result interface{}, taskType async.TaskType, err error) {
|
||||
res := GetOwnedCollectiblesResponse{
|
||||
ErrorCode: ErrorCodeFailed,
|
||||
}
|
||||
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, async.ErrTaskOverwritten) {
|
||||
res.ErrorCode = ErrorCodeTaskCanceled
|
||||
} else if err == nil {
|
||||
fnRet := result.(*GetOwnedCollectiblesReturnType)
|
||||
|
||||
if err == nil {
|
||||
res.Collectibles = fnRet.collectibles
|
||||
res.Offset = offset
|
||||
res.HasMore = fnRet.hasMore
|
||||
res.OwnershipStatus = fnRet.ownershipStatus
|
||||
res.ErrorCode = ErrorCodeSuccess
|
||||
}
|
||||
}
|
||||
|
||||
s.sendResponseEvent(&requestID, EventOwnedCollectiblesFilteringDone, res, err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) GetCollectiblesByUniqueID(
|
||||
ctx context.Context,
|
||||
uniqueIDs []thirdparty.CollectibleUniqueID,
|
||||
dataType CollectibleDataType) (*GetCollectiblesByUniqueIDReturnType, error) {
|
||||
collectibles, err := s.collectibleIDsToDataType(ctx, uniqueIDs, dataType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &GetCollectiblesByUniqueIDReturnType{
|
||||
collectibles: collectibles,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (s *Service) GetCollectiblesByUniqueIDAsync(
|
||||
requestID int32,
|
||||
uniqueIDs []thirdparty.CollectibleUniqueID,
|
||||
dataType CollectibleDataType) {
|
||||
s.scheduler.Enqueue(requestID, getCollectiblesDataTask, func(ctx context.Context) (interface{}, error) {
|
||||
return s.GetCollectiblesByUniqueID(ctx, uniqueIDs, dataType)
|
||||
}, func(result interface{}, taskType async.TaskType, err error) {
|
||||
res := GetCollectiblesByUniqueIDResponse{
|
||||
ErrorCode: ErrorCodeFailed,
|
||||
}
|
||||
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, async.ErrTaskOverwritten) {
|
||||
res.ErrorCode = ErrorCodeTaskCanceled
|
||||
} else if err == nil {
|
||||
fnRet := result.(*GetCollectiblesByUniqueIDReturnType)
|
||||
|
||||
if err == nil {
|
||||
res.Collectibles = fnRet.collectibles
|
||||
res.ErrorCode = ErrorCodeSuccess
|
||||
}
|
||||
}
|
||||
|
||||
s.sendResponseEvent(&requestID, EventGetCollectiblesDetailsDone, res, err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) RefetchOwnedCollectibles() {
|
||||
s.controller.RefetchOwnedCollectibles()
|
||||
}
|
||||
|
||||
func (s *Service) Start() {
|
||||
s.controller.Start()
|
||||
}
|
||||
|
||||
func (s *Service) Stop() {
|
||||
s.controller.Stop()
|
||||
|
||||
s.scheduler.Stop()
|
||||
}
|
||||
|
||||
func (s *Service) sendResponseEvent(requestID *int32, eventType walletevent.EventType, payloadObj interface{}, resErr error) {
|
||||
payload, err := json.Marshal(payloadObj)
|
||||
if err != nil {
|
||||
log.Error("Error marshaling response: %v; result error: %w", err, resErr)
|
||||
} else {
|
||||
err = resErr
|
||||
}
|
||||
|
||||
log.Debug("wallet.api.collectibles.Service RESPONSE", "requestID", requestID, "eventType", eventType, "error", err, "payload.len", len(payload))
|
||||
|
||||
event := walletevent.Event{
|
||||
Type: eventType,
|
||||
Message: string(payload),
|
||||
}
|
||||
|
||||
if requestID != nil {
|
||||
event.RequestID = new(int)
|
||||
*event.RequestID = int(*requestID)
|
||||
}
|
||||
|
||||
s.walletFeed.Send(event)
|
||||
}
|
||||
|
||||
func (s *Service) FilterOwnedCollectibles(chainIDs []walletCommon.ChainID, owners []common.Address, filter Filter, offset int, limit int) ([]thirdparty.CollectibleUniqueID, bool, error) {
|
||||
ctx := context.Background()
|
||||
// Request one more than limit, to check if DB has more available
|
||||
ids, err := filterOwnedCollectibles(ctx, s.db, chainIDs, owners, filter, offset, limit+1)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
hasMore := len(ids) > limit
|
||||
if hasMore {
|
||||
ids = ids[:limit]
|
||||
}
|
||||
|
||||
return ids, hasMore, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetOwnedCollectible(chainID walletCommon.ChainID, owner common.Address, contractAddress common.Address, tokenID *big.Int) (*thirdparty.CollectibleUniqueID, error) {
|
||||
return s.ownershipDB.GetOwnedCollectible(chainID, owner, contractAddress, tokenID)
|
||||
}
|
||||
|
||||
func (s *Service) GetOwnershipStatus(chainIDs []walletCommon.ChainID, owners []common.Address) (OwnershipStatusPerAddressAndChainID, error) {
|
||||
ret := make(OwnershipStatusPerAddressAndChainID)
|
||||
for _, address := range owners {
|
||||
ret[address] = make(OwnershipStatusPerChainID)
|
||||
for _, chainID := range chainIDs {
|
||||
timestamp, err := s.ownershipDB.GetOwnershipUpdateTimestamp(address, chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[address][chainID] = OwnershipStatus{
|
||||
State: s.controller.GetCommandState(chainID, address),
|
||||
Timestamp: timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (s *Service) collectibleIDsToDataType(ctx context.Context, ids []thirdparty.CollectibleUniqueID, dataType CollectibleDataType) ([]Collectible, error) {
|
||||
switch dataType {
|
||||
case CollectibleDataTypeUniqueID:
|
||||
return idsToCollectibles(ids), nil
|
||||
case CollectibleDataTypeHeader, CollectibleDataTypeDetails, CollectibleDataTypeCommunityHeader:
|
||||
collectibles, err := s.manager.FetchAssetsByCollectibleUniqueID(ctx, ids, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch dataType {
|
||||
case CollectibleDataTypeHeader:
|
||||
return fullCollectiblesDataToHeaders(collectibles), nil
|
||||
case CollectibleDataTypeDetails:
|
||||
return fullCollectiblesDataToDetails(collectibles), nil
|
||||
case CollectibleDataTypeCommunityHeader:
|
||||
return fullCollectiblesDataToCommunityHeader(collectibles), nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("unknown data type")
|
||||
}
|
||||
|
||||
func (s *Service) onOwnedCollectiblesChange(ownedCollectiblesChange OwnedCollectiblesChange) {
|
||||
// Try to find a matching transfer for newly added/updated collectibles
|
||||
switch ownedCollectiblesChange.changeType {
|
||||
case OwnedCollectiblesChangeTypeAdded, OwnedCollectiblesChangeTypeUpdated:
|
||||
// For recently added/updated collectibles, try to find a matching transfer
|
||||
s.lookupTransferForCollectibles(ownedCollectiblesChange.ownedCollectibles)
|
||||
s.notifyCommunityCollectiblesReceived(ownedCollectiblesChange.ownedCollectibles)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) onCollectiblesTransfer(account common.Address, chainID walletCommon.ChainID, transfers []transfer.Transfer) {
|
||||
for _, transfer := range transfers {
|
||||
// If Collectible is already in the DB, update transfer ID with the latest detected transfer
|
||||
id := thirdparty.CollectibleUniqueID{
|
||||
ContractID: thirdparty.ContractID{
|
||||
ChainID: chainID,
|
||||
Address: transfer.Log.Address,
|
||||
},
|
||||
TokenID: &bigint.BigInt{Int: transfer.TokenID},
|
||||
}
|
||||
err := s.manager.SetCollectibleTransferID(account, id, transfer.ID, true)
|
||||
if err != nil {
|
||||
log.Error("Error setting transfer ID for collectible", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) lookupTransferForCollectibles(ownedCollectibles OwnedCollectibles) {
|
||||
// There are some limitations to this approach:
|
||||
// - Collectibles ownership and transfers are not in sync and might represent the state at different moments.
|
||||
// - We have no way of knowing if the latest collectible transfer we've detected is actually the latest one, so the timestamp we
|
||||
// use might be older than the real one.
|
||||
// - There might be detected transfers that are temporarily not reflected in the collectibles ownership.
|
||||
// - For ERC721 tokens we should only look for incoming transfers. For ERC1155 tokens we should look for both incoming and outgoing transfers.
|
||||
// We need to get the contract standard for each collectible to know which approach to take.
|
||||
for _, id := range ownedCollectibles.ids {
|
||||
transfer, err := s.transferDB.GetLatestCollectibleTransfer(ownedCollectibles.account, id)
|
||||
if err != nil {
|
||||
log.Error("Error fetching latest collectible transfer", "error", err)
|
||||
continue
|
||||
}
|
||||
if transfer != nil {
|
||||
err = s.manager.SetCollectibleTransferID(ownedCollectibles.account, id, transfer.ID, false)
|
||||
if err != nil {
|
||||
log.Error("Error setting transfer ID for collectible", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) notifyCommunityCollectiblesReceived(ownedCollectibles OwnedCollectibles) {
|
||||
ctx := context.Background()
|
||||
|
||||
collectiblesData, err := s.manager.FetchAssetsByCollectibleUniqueID(ctx, ownedCollectibles.ids, false)
|
||||
if err != nil {
|
||||
log.Error("Error fetching collectibles data", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
communityCollectibles := fullCollectiblesDataToCommunityHeader(collectiblesData)
|
||||
|
||||
if len(communityCollectibles) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
encodedMessage, err := json.Marshal(communityCollectibles)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.walletFeed.Send(walletevent.Event{
|
||||
Type: EventCommunityCollectiblesReceived,
|
||||
ChainID: uint64(ownedCollectibles.chainID),
|
||||
Accounts: []common.Address{
|
||||
ownedCollectibles.account,
|
||||
},
|
||||
Message: string(encodedMessage),
|
||||
})
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
package collectibles
|
||||
|
||||
import (
|
||||
"github.com/status-im/status-go/protocol/communities/token"
|
||||
w_common "github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
)
|
||||
|
||||
// Combined Collection+Collectible info, used to display a detailed view of a collectible
|
||||
type Collectible struct {
|
||||
DataType CollectibleDataType `json:"data_type"`
|
||||
ID thirdparty.CollectibleUniqueID `json:"id"`
|
||||
ContractType w_common.ContractType `json:"contract_type"`
|
||||
CollectibleData *CollectibleData `json:"collectible_data,omitempty"`
|
||||
CollectionData *CollectionData `json:"collection_data,omitempty"`
|
||||
CommunityData *CommunityData `json:"community_data,omitempty"`
|
||||
Ownership []thirdparty.AccountBalance `json:"ownership,omitempty"`
|
||||
}
|
||||
|
||||
type CollectibleData struct {
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
ImageURL *string `json:"image_url,omitempty"`
|
||||
AnimationURL *string `json:"animation_url,omitempty"`
|
||||
AnimationMediaType *string `json:"animation_media_type,omitempty"`
|
||||
Traits *[]thirdparty.CollectibleTrait `json:"traits,omitempty"`
|
||||
BackgroundColor *string `json:"background_color,omitempty"`
|
||||
}
|
||||
|
||||
type CollectionData struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
ImageURL string `json:"image_url"`
|
||||
}
|
||||
|
||||
type CommunityData struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color"`
|
||||
PrivilegesLevel token.PrivilegesLevel `json:"privileges_level"`
|
||||
ImageURL *string `json:"image_url,omitempty"`
|
||||
}
|
||||
|
||||
func idToCollectible(id thirdparty.CollectibleUniqueID) Collectible {
|
||||
ret := Collectible{
|
||||
DataType: CollectibleDataTypeUniqueID,
|
||||
ID: id,
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func idsToCollectibles(ids []thirdparty.CollectibleUniqueID) []Collectible {
|
||||
res := make([]Collectible, 0, len(ids))
|
||||
|
||||
for _, id := range ids {
|
||||
c := idToCollectible(id)
|
||||
res = append(res, c)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func getContractType(c thirdparty.FullCollectibleData) w_common.ContractType {
|
||||
if c.CollectibleData.ContractType != w_common.ContractTypeUnknown {
|
||||
return c.CollectibleData.ContractType
|
||||
}
|
||||
if c.CollectionData != nil && c.CollectionData.ContractType != w_common.ContractTypeUnknown {
|
||||
return c.CollectionData.ContractType
|
||||
}
|
||||
return w_common.ContractTypeUnknown
|
||||
}
|
||||
|
||||
func fullCollectibleDataToHeader(c thirdparty.FullCollectibleData) Collectible {
|
||||
ret := Collectible{
|
||||
DataType: CollectibleDataTypeHeader,
|
||||
ID: c.CollectibleData.ID,
|
||||
ContractType: getContractType(c),
|
||||
CollectibleData: &CollectibleData{
|
||||
Name: c.CollectibleData.Name,
|
||||
ImageURL: &c.CollectibleData.ImageURL,
|
||||
AnimationURL: &c.CollectibleData.AnimationURL,
|
||||
AnimationMediaType: &c.CollectibleData.AnimationMediaType,
|
||||
BackgroundColor: &c.CollectibleData.BackgroundColor,
|
||||
},
|
||||
}
|
||||
if c.CollectionData != nil {
|
||||
ret.CollectionData = &CollectionData{
|
||||
Name: c.CollectionData.Name,
|
||||
Slug: c.CollectionData.Slug,
|
||||
ImageURL: c.CollectionData.ImageURL,
|
||||
}
|
||||
}
|
||||
if c.CollectibleData.CommunityID != "" {
|
||||
communityData := communityInfoToData(c.CollectibleData.CommunityID, c.CommunityInfo, c.CollectibleCommunityInfo)
|
||||
ret.CommunityData = &communityData
|
||||
}
|
||||
ret.Ownership = c.Ownership
|
||||
return ret
|
||||
}
|
||||
|
||||
func fullCollectiblesDataToHeaders(data []thirdparty.FullCollectibleData) []Collectible {
|
||||
res := make([]Collectible, 0, len(data))
|
||||
|
||||
for _, c := range data {
|
||||
header := fullCollectibleDataToHeader(c)
|
||||
res = append(res, header)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func fullCollectibleDataToDetails(c thirdparty.FullCollectibleData) Collectible {
|
||||
ret := Collectible{
|
||||
DataType: CollectibleDataTypeDetails,
|
||||
ID: c.CollectibleData.ID,
|
||||
ContractType: getContractType(c),
|
||||
CollectibleData: &CollectibleData{
|
||||
Name: c.CollectibleData.Name,
|
||||
Description: &c.CollectibleData.Description,
|
||||
ImageURL: &c.CollectibleData.ImageURL,
|
||||
AnimationURL: &c.CollectibleData.AnimationURL,
|
||||
AnimationMediaType: &c.CollectibleData.AnimationMediaType,
|
||||
BackgroundColor: &c.CollectibleData.BackgroundColor,
|
||||
Traits: &c.CollectibleData.Traits,
|
||||
},
|
||||
}
|
||||
if c.CollectionData != nil {
|
||||
ret.CollectionData = &CollectionData{
|
||||
Name: c.CollectionData.Name,
|
||||
Slug: c.CollectionData.Slug,
|
||||
ImageURL: c.CollectionData.ImageURL,
|
||||
}
|
||||
}
|
||||
if c.CollectibleData.CommunityID != "" {
|
||||
communityData := communityInfoToData(c.CollectibleData.CommunityID, c.CommunityInfo, c.CollectibleCommunityInfo)
|
||||
ret.CommunityData = &communityData
|
||||
}
|
||||
ret.Ownership = c.Ownership
|
||||
return ret
|
||||
}
|
||||
|
||||
func fullCollectiblesDataToDetails(data []thirdparty.FullCollectibleData) []Collectible {
|
||||
res := make([]Collectible, 0, len(data))
|
||||
|
||||
for _, c := range data {
|
||||
details := fullCollectibleDataToDetails(c)
|
||||
res = append(res, details)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func fullCollectiblesDataToCommunityHeader(data []thirdparty.FullCollectibleData) []Collectible {
|
||||
res := make([]Collectible, 0, len(data))
|
||||
|
||||
for _, c := range data {
|
||||
collectibleID := c.CollectibleData.ID
|
||||
communityID := c.CollectibleData.CommunityID
|
||||
|
||||
if communityID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
communityData := communityInfoToData(communityID, c.CommunityInfo, c.CollectibleCommunityInfo)
|
||||
|
||||
header := Collectible{
|
||||
ID: collectibleID,
|
||||
ContractType: getContractType(c),
|
||||
CollectibleData: &CollectibleData{
|
||||
Name: c.CollectibleData.Name,
|
||||
},
|
||||
CommunityData: &communityData,
|
||||
Ownership: c.Ownership,
|
||||
}
|
||||
|
||||
res = append(res, header)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func communityInfoToData(communityID string, community *thirdparty.CommunityInfo, communityCollectible *thirdparty.CollectibleCommunityInfo) CommunityData {
|
||||
ret := CommunityData{
|
||||
ID: communityID,
|
||||
}
|
||||
|
||||
if community != nil {
|
||||
ret.Name = community.CommunityName
|
||||
ret.Color = community.CommunityColor
|
||||
ret.ImageURL = &community.CommunityImage
|
||||
}
|
||||
|
||||
if communityCollectible != nil {
|
||||
ret.PrivilegesLevel = communityCollectible.PrivilegesLevel
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
Reference in New Issue
Block a user