chore: implement collectibles data db cache
This commit is contained in:
parent
4fd94c2345
commit
33c116f7b1
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,51 @@
|
|||
CREATE TABLE IF NOT EXISTS collectible_data_cache (
|
||||
chain_id UNSIGNED BIGINT NOT NULL,
|
||||
contract_address VARCHAR NOT NULL,
|
||||
token_id BLOB NOT NULL,
|
||||
provider VARCHAR NOT NULL,
|
||||
name VARCHAR NOT NULL,
|
||||
description VARCHAR NOT NULL,
|
||||
permalink VARCHAR NOT NULL,
|
||||
image_url VARCHAR NOT NULL,
|
||||
animation_url VARCHAR NOT NULL,
|
||||
animation_media_type VARCHAR NOT NULL,
|
||||
background_color VARCHAR NOT NULL,
|
||||
token_uri VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS collectible_data_identify_entry ON collectible_data_cache (chain_id, contract_address, token_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS collectible_traits_cache (
|
||||
chain_id UNSIGNED BIGINT NOT NULL,
|
||||
contract_address VARCHAR NOT NULL,
|
||||
token_id BLOB NOT NULL,
|
||||
trait_type VARCHAR NOT NULL,
|
||||
trait_value VARCHAR NOT NULL,
|
||||
display_type VARCHAR NOT NULL,
|
||||
max_value VARCHAR NOT NULL,
|
||||
FOREIGN KEY(chain_id, contract_address, token_id) REFERENCES collectible_data_cache(chain_id, contract_address, token_id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS collection_data_cache (
|
||||
chain_id UNSIGNED BIGINT NOT NULL,
|
||||
contract_address VARCHAR NOT NULL,
|
||||
provider VARCHAR NOT NULL,
|
||||
name VARCHAR NOT NULL,
|
||||
slug VARCHAR NOT NULL,
|
||||
image_url VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS collection_data_identify_entry ON collection_data_cache (chain_id, contract_address);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS collection_traits_cache (
|
||||
chain_id UNSIGNED BIGINT NOT NULL,
|
||||
contract_address VARCHAR NOT NULL,
|
||||
trait_type VARCHAR NOT NULL,
|
||||
min REAL NOT NULL,
|
||||
max REAL NOT NULL,
|
||||
FOREIGN KEY(chain_id, contract_address) REFERENCES collection_data_cache(chain_id, contract_address)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
);
|
|
@ -0,0 +1,252 @@
|
|||
package collectibles
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"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, animation_url, animation_media_type, background_color, token_uri"
|
||||
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 upsertCollectiblesData(creator sqlite.StatementCreator, collectibles []thirdparty.CollectibleData) error {
|
||||
insertCollectible, err := creator.Prepare(fmt.Sprintf(`INSERT OR REPLACE INTO collectible_data_cache (%s)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 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.AnimationURL,
|
||||
c.AnimationMediaType,
|
||||
c.BackgroundColor,
|
||||
c.TokenURI,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = upsertCollectibleTraits(creator, c.ID, c.Traits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *CollectibleDataDB) SetData(collectibles []thirdparty.CollectibleData) (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 = upsertCollectiblesData(tx, collectibles)
|
||||
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.AnimationURL,
|
||||
&c.AnimationMediaType,
|
||||
&c.BackgroundColor,
|
||||
&c.TokenURI,
|
||||
)
|
||||
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
|
||||
}
|
||||
ret[c.ID.HashKey()] = *c
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
package collectibles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/status-im/status-go/appdatabase"
|
||||
"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/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupCollectibleDataDBTest(t *testing.T) (*CollectibleDataDB, func()) {
|
||||
db, err := appdatabase.InitializeDB(":memory:", "wallet-collectibles-data-db-tests", 1)
|
||||
require.NoError(t, err)
|
||||
return NewCollectibleDataDB(db), func() {
|
||||
require.NoError(t, db.Close())
|
||||
}
|
||||
}
|
||||
|
||||
func generateTestCollectiblesData(count int) (result []thirdparty.CollectibleData) {
|
||||
result = make([]thirdparty.CollectibleData, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
bigI := big.NewInt(int64(count))
|
||||
newCollectible := thirdparty.CollectibleData{
|
||||
ID: thirdparty.CollectibleUniqueID{
|
||||
ContractID: thirdparty.ContractID{
|
||||
ChainID: w_common.ChainID(i),
|
||||
Address: common.BigToAddress(bigI),
|
||||
},
|
||||
TokenID: &bigint.BigInt{Int: bigI},
|
||||
},
|
||||
Provider: fmt.Sprintf("provider-%d", i),
|
||||
Name: fmt.Sprintf("name-%d", i),
|
||||
Description: fmt.Sprintf("description-%d", i),
|
||||
Permalink: fmt.Sprintf("permalink-%d", i),
|
||||
ImageURL: fmt.Sprintf("imageurl-%d", i),
|
||||
AnimationURL: fmt.Sprintf("animationurl-%d", i),
|
||||
AnimationMediaType: fmt.Sprintf("animationmediatype-%d", i),
|
||||
Traits: []thirdparty.CollectibleTrait{
|
||||
{
|
||||
TraitType: fmt.Sprintf("traittype-%d", i),
|
||||
Value: fmt.Sprintf("traitvalue-%d", i),
|
||||
DisplayType: fmt.Sprintf("displaytype-%d", i),
|
||||
MaxValue: fmt.Sprintf("maxvalue-%d", i),
|
||||
},
|
||||
{
|
||||
TraitType: fmt.Sprintf("traittype-%d", i),
|
||||
Value: fmt.Sprintf("traitvalue-%d", i),
|
||||
DisplayType: fmt.Sprintf("displaytype-%d", i),
|
||||
MaxValue: fmt.Sprintf("maxvalue-%d", i),
|
||||
},
|
||||
{
|
||||
TraitType: fmt.Sprintf("traittype-%d", i),
|
||||
Value: fmt.Sprintf("traitvalue-%d", i),
|
||||
DisplayType: fmt.Sprintf("displaytype-%d", i),
|
||||
MaxValue: fmt.Sprintf("maxvalue-%d", i),
|
||||
},
|
||||
},
|
||||
BackgroundColor: fmt.Sprintf("backgroundcolor-%d", i),
|
||||
TokenURI: fmt.Sprintf("tokenuri-%d", i),
|
||||
}
|
||||
result = append(result, newCollectible)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestUpdateCollectiblesData(t *testing.T) {
|
||||
db, cleanDB := setupCollectibleDataDBTest(t)
|
||||
defer cleanDB()
|
||||
|
||||
data := generateTestCollectiblesData(50)
|
||||
|
||||
var err error
|
||||
|
||||
err = db.SetData(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
ids := make([]thirdparty.CollectibleUniqueID, 0, len(data))
|
||||
for _, collectible := range data {
|
||||
ids = append(ids, collectible.ID)
|
||||
}
|
||||
|
||||
// Check for missing IDs
|
||||
idsNotInDB, err := db.GetIDsNotInDB(ids)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, idsNotInDB)
|
||||
|
||||
extraID0 := thirdparty.CollectibleUniqueID{
|
||||
ContractID: thirdparty.ContractID{
|
||||
ChainID: w_common.ChainID(100),
|
||||
Address: common.BigToAddress(big.NewInt(100)),
|
||||
},
|
||||
TokenID: &bigint.BigInt{Int: big.NewInt(100)},
|
||||
}
|
||||
extraID1 := thirdparty.CollectibleUniqueID{
|
||||
ContractID: thirdparty.ContractID{
|
||||
ChainID: w_common.ChainID(101),
|
||||
Address: common.BigToAddress(big.NewInt(101)),
|
||||
},
|
||||
TokenID: &bigint.BigInt{Int: big.NewInt(101)},
|
||||
}
|
||||
extraIds := []thirdparty.CollectibleUniqueID{extraID0, extraID1}
|
||||
|
||||
idsNotInDB, err = db.GetIDsNotInDB(extraIds)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, extraIds, idsNotInDB)
|
||||
|
||||
combinedIds := append(ids, extraIds...)
|
||||
idsNotInDB, err = db.GetIDsNotInDB(combinedIds)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, extraIds, idsNotInDB)
|
||||
|
||||
// Check for loaded data
|
||||
loadedMap, err := db.GetData(ids)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(ids), len(loadedMap))
|
||||
|
||||
for _, origC := range data {
|
||||
require.Equal(t, origC, loadedMap[origC.ID.HashKey()])
|
||||
}
|
||||
|
||||
// update some collectibles, changing the provider
|
||||
c0 := data[0]
|
||||
c0.Name = "new collectible name 0"
|
||||
c0.Provider = "new collectible provider 0"
|
||||
|
||||
c1 := data[1]
|
||||
c1.Name = "new collectible name 1"
|
||||
c1.Provider = "new collectible provider 1"
|
||||
|
||||
err = db.SetData([]thirdparty.CollectibleData{c0, c1})
|
||||
require.NoError(t, err)
|
||||
|
||||
loadedMap, err = db.GetData([]thirdparty.CollectibleUniqueID{c0.ID, c1.ID})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(loadedMap))
|
||||
|
||||
require.Equal(t, c0, loadedMap[c0.ID.HashKey()])
|
||||
require.Equal(t, c1, loadedMap[c1.ID.HashKey()])
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
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"
|
||||
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 upsertCollectionsData(creator sqlite.StatementCreator, collections []thirdparty.CollectionData) error {
|
||||
insertCollection, err := creator.Prepare(fmt.Sprintf(`INSERT OR REPLACE INTO collection_data_cache (%s)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`, 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,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = upsertCollectionTraits(creator, c.ID, c.Traits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *CollectionDataDB) SetData(collections []thirdparty.CollectionData) (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 = upsertCollectionsData(tx, collections)
|
||||
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,
|
||||
)
|
||||
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
|
||||
}
|
||||
ret[c.ID.HashKey()] = *c
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package collectibles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/status-im/status-go/appdatabase"
|
||||
w_common "github.com/status-im/status-go/services/wallet/common"
|
||||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupCollectionDataDBTest(t *testing.T) (*CollectionDataDB, func()) {
|
||||
db, err := appdatabase.InitializeDB(":memory:", "wallet-collections-data-db-tests", 1)
|
||||
require.NoError(t, err)
|
||||
return NewCollectionDataDB(db), func() {
|
||||
require.NoError(t, db.Close())
|
||||
}
|
||||
}
|
||||
|
||||
func generateTestCollectionsData(count int) (result []thirdparty.CollectionData) {
|
||||
result = make([]thirdparty.CollectionData, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
bigI := big.NewInt(int64(count))
|
||||
traits := make(map[string]thirdparty.CollectionTrait)
|
||||
for j := 0; j < 3; j++ {
|
||||
traits[fmt.Sprintf("traittype-%d", j)] = thirdparty.CollectionTrait{
|
||||
Min: float64(i+j) / 2,
|
||||
Max: float64(i+j) * 2,
|
||||
}
|
||||
}
|
||||
|
||||
newCollection := thirdparty.CollectionData{
|
||||
ID: thirdparty.ContractID{
|
||||
ChainID: w_common.ChainID(i),
|
||||
Address: common.BigToAddress(bigI),
|
||||
},
|
||||
Provider: fmt.Sprintf("provider-%d", i),
|
||||
Name: fmt.Sprintf("name-%d", i),
|
||||
Slug: fmt.Sprintf("slug-%d", i),
|
||||
ImageURL: fmt.Sprintf("imageurl-%d", i),
|
||||
Traits: traits,
|
||||
}
|
||||
result = append(result, newCollection)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestUpdateCollectionsData(t *testing.T) {
|
||||
db, cleanDB := setupCollectionDataDBTest(t)
|
||||
defer cleanDB()
|
||||
|
||||
data := generateTestCollectionsData(50)
|
||||
|
||||
var err error
|
||||
|
||||
err = db.SetData(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
ids := make([]thirdparty.ContractID, 0, len(data))
|
||||
for _, collection := range data {
|
||||
ids = append(ids, collection.ID)
|
||||
}
|
||||
|
||||
// Check for missing IDs
|
||||
idsNotInDB, err := db.GetIDsNotInDB(ids)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, idsNotInDB)
|
||||
|
||||
extraID0 := thirdparty.ContractID{
|
||||
ChainID: w_common.ChainID(100),
|
||||
Address: common.BigToAddress(big.NewInt(100)),
|
||||
}
|
||||
extraID1 := thirdparty.ContractID{
|
||||
ChainID: w_common.ChainID(101),
|
||||
Address: common.BigToAddress(big.NewInt(101)),
|
||||
}
|
||||
extraIds := []thirdparty.ContractID{extraID0, extraID1}
|
||||
|
||||
idsNotInDB, err = db.GetIDsNotInDB(extraIds)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, extraIds, idsNotInDB)
|
||||
|
||||
combinedIds := append(ids, extraIds...)
|
||||
idsNotInDB, err = db.GetIDsNotInDB(combinedIds)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, extraIds, idsNotInDB)
|
||||
|
||||
// Check for loaded data
|
||||
loadedMap, err := db.GetData(ids)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(ids), len(loadedMap))
|
||||
|
||||
for _, origC := range data {
|
||||
require.Equal(t, origC, loadedMap[origC.ID.HashKey()])
|
||||
}
|
||||
|
||||
// update some collections, changing the provider
|
||||
c0 := data[0]
|
||||
c0.Name = "new collection name 0"
|
||||
c0.Provider = "new collection provider 0"
|
||||
|
||||
c1 := data[1]
|
||||
c1.Name = "new collection name 1"
|
||||
c1.Provider = "new collection provider 1"
|
||||
|
||||
err = db.SetData([]thirdparty.CollectionData{c0, c1})
|
||||
require.NoError(t, err)
|
||||
|
||||
loadedMap, err = db.GetData([]thirdparty.ContractID{c0.ID, c1.ID})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(loadedMap))
|
||||
|
||||
require.Equal(t, c0, loadedMap[c0.ID.HashKey()])
|
||||
require.Equal(t, c1, loadedMap[c1.ID.HashKey()])
|
||||
}
|
|
@ -12,6 +12,7 @@ import (
|
|||
"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"
|
||||
)
|
||||
|
||||
type OwnershipDB struct {
|
||||
|
@ -27,12 +28,7 @@ func NewOwnershipDB(sqlDb *sql.DB) *OwnershipDB {
|
|||
const ownershipColumns = "chain_id, contract_address, token_id, owner_address"
|
||||
const selectOwnershipColumns = "chain_id, contract_address, token_id"
|
||||
|
||||
// statementCreator allows to pass transaction or database to use in consumer.
|
||||
type statementCreator interface {
|
||||
Prepare(query string) (*sql.Stmt, error)
|
||||
}
|
||||
|
||||
func removeAddressOwnership(creator statementCreator, chainID w_common.ChainID, ownerAddress common.Address) error {
|
||||
func removeAddressOwnership(creator sqlite.StatementCreator, chainID w_common.ChainID, ownerAddress common.Address) error {
|
||||
deleteOwnership, err := creator.Prepare("DELETE FROM collectibles_ownership_cache WHERE chain_id = ? AND owner_address = ?")
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -46,7 +42,7 @@ func removeAddressOwnership(creator statementCreator, chainID w_common.ChainID,
|
|||
return nil
|
||||
}
|
||||
|
||||
func insertAddressOwnership(creator statementCreator, ownerAddress common.Address, collectibles []thirdparty.CollectibleUniqueID) error {
|
||||
func insertAddressOwnership(creator sqlite.StatementCreator, ownerAddress common.Address, collectibles []thirdparty.CollectibleUniqueID) error {
|
||||
insertOwnership, err := creator.Prepare(fmt.Sprintf(`INSERT INTO collectibles_ownership_cache (%s)
|
||||
VALUES (?, ?, ?, ?)`, ownershipColumns))
|
||||
if err != nil {
|
||||
|
|
|
@ -15,7 +15,6 @@ import (
|
|||
"github.com/status-im/status-go/services/wallet/thirdparty"
|
||||
)
|
||||
|
||||
const AlchemyID = "alchemy"
|
||||
const nftMetadataBatchLimit = 100
|
||||
const contractMetadataBatchLimit = 100
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@ import (
|
|||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
const AlchemyID = "alchemy"
|
||||
|
||||
type TokenBalance struct {
|
||||
TokenID *bigint.BigInt `json:"tokenId"`
|
||||
Balance *bigint.BigInt `json:"balance"`
|
||||
|
@ -155,6 +157,7 @@ func alchemyToCollectibleTraits(attributes []Attribute) []thirdparty.Collectible
|
|||
func (c *Contract) toCollectionData(id thirdparty.ContractID) thirdparty.CollectionData {
|
||||
ret := thirdparty.CollectionData{
|
||||
ID: id,
|
||||
Provider: AlchemyID,
|
||||
Name: c.Name,
|
||||
ImageURL: c.OpenSeaMetadata.ImageURL,
|
||||
}
|
||||
|
@ -164,6 +167,7 @@ func (c *Contract) toCollectionData(id thirdparty.ContractID) thirdparty.Collect
|
|||
func (c *Asset) toCollectiblesData(id thirdparty.CollectibleUniqueID) thirdparty.CollectibleData {
|
||||
return thirdparty.CollectibleData{
|
||||
ID: id,
|
||||
Provider: AlchemyID,
|
||||
Name: c.Name,
|
||||
Description: c.Description,
|
||||
ImageURL: c.Image.ImageURL,
|
||||
|
|
|
@ -73,6 +73,7 @@ type CollectionTrait struct {
|
|||
// Collection info
|
||||
type CollectionData struct {
|
||||
ID ContractID `json:"id"`
|
||||
Provider string `json:"provider"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
ImageURL string `json:"image_url"`
|
||||
|
@ -89,6 +90,7 @@ type CollectibleTrait struct {
|
|||
// Collectible info
|
||||
type CollectibleData struct {
|
||||
ID CollectibleUniqueID `json:"id"`
|
||||
Provider string `json:"provider"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Permalink string `json:"permalink"`
|
||||
|
|
|
@ -15,7 +15,6 @@ import (
|
|||
)
|
||||
|
||||
const baseURL = "https://nft.api.infura.io"
|
||||
const InfuraID = "infura"
|
||||
|
||||
type Client struct {
|
||||
thirdparty.CollectibleContractOwnershipProvider
|
||||
|
|
|
@ -15,6 +15,8 @@ import (
|
|||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
const InfuraID = "infura"
|
||||
|
||||
func chainStringToChainID(chainString string) walletCommon.ChainID {
|
||||
chainID := walletCommon.UnknownChainID
|
||||
switch chainString {
|
||||
|
@ -129,6 +131,7 @@ type NFTList struct {
|
|||
func (c *Asset) toCollectiblesData(id thirdparty.CollectibleUniqueID) thirdparty.CollectibleData {
|
||||
return thirdparty.CollectibleData{
|
||||
ID: id,
|
||||
Provider: InfuraID,
|
||||
Name: c.Metadata.Name,
|
||||
Description: c.Metadata.Description,
|
||||
Permalink: c.Metadata.Permalink,
|
||||
|
@ -177,7 +180,8 @@ func infuraToCollectibleTraits(attributes []Attribute) []thirdparty.CollectibleT
|
|||
|
||||
func (c *ContractMetadata) toCommon(id thirdparty.ContractID) thirdparty.CollectionData {
|
||||
return thirdparty.CollectionData{
|
||||
ID: id,
|
||||
Name: c.Name,
|
||||
ID: id,
|
||||
Provider: InfuraID,
|
||||
Name: c.Name,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,6 @@ const (
|
|||
EventCollectibleStatusChanged walletevent.EventType = "wallet-collectible-opensea-v1-status-changed"
|
||||
)
|
||||
|
||||
const OpenseaV1ID = "openseaV1"
|
||||
|
||||
const AssetLimit = 200
|
||||
const CollectionLimit = 300
|
||||
|
||||
|
|
|
@ -117,6 +117,7 @@ func TestFetchAllAssetsByOwnerAndCollection(t *testing.T) {
|
|||
},
|
||||
TokenID: &bigint.BigInt{Int: big.NewInt(1)},
|
||||
},
|
||||
Provider: "openseaV1",
|
||||
Name: "Rocky",
|
||||
Description: "Rocky Balboa",
|
||||
Permalink: "permalink",
|
||||
|
@ -128,8 +129,9 @@ func TestFetchAllAssetsByOwnerAndCollection(t *testing.T) {
|
|||
ChainID: 1,
|
||||
Address: common.HexToAddress("0x1"),
|
||||
},
|
||||
Name: "Rocky",
|
||||
Traits: map[string]thirdparty.CollectionTrait{},
|
||||
Provider: "openseaV1",
|
||||
Name: "Rocky",
|
||||
Traits: map[string]thirdparty.CollectionTrait{},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -15,6 +15,8 @@ import (
|
|||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
const OpenseaV1ID = "openseaV1"
|
||||
|
||||
func chainStringToChainID(chainString string) walletCommon.ChainID {
|
||||
chainID := walletCommon.UnknownChainID
|
||||
switch chainString {
|
||||
|
@ -159,6 +161,7 @@ func openseaToCollectibleTraits(traits []Trait) []thirdparty.CollectibleTrait {
|
|||
func (c *Collection) toCollectionData(id thirdparty.ContractID) thirdparty.CollectionData {
|
||||
ret := thirdparty.CollectionData{
|
||||
ID: id,
|
||||
Provider: OpenseaV1ID,
|
||||
Name: c.Name,
|
||||
Slug: c.Slug,
|
||||
ImageURL: c.ImageURL,
|
||||
|
@ -176,6 +179,7 @@ func (c *Collection) toCollectionData(id thirdparty.ContractID) thirdparty.Colle
|
|||
func (c *Asset) toCollectiblesData() thirdparty.CollectibleData {
|
||||
return thirdparty.CollectibleData{
|
||||
ID: c.id(),
|
||||
Provider: OpenseaV1ID,
|
||||
Name: c.Name,
|
||||
Description: c.Description,
|
||||
Permalink: c.Permalink,
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package sqlite
|
||||
|
||||
import "database/sql"
|
||||
|
||||
// statementCreator allows to pass transaction or database to use in consumer.
|
||||
type StatementCreator interface {
|
||||
Prepare(query string) (*sql.Stmt, error)
|
||||
}
|
Loading…
Reference in New Issue