github.com/status-im/status-go@v1.1.0/services/wallet/collectibles/collectible_data_db.go (about)

     1  package collectibles
     2  
     3  import (
     4  	"database/sql"
     5  	"fmt"
     6  	"math/big"
     7  
     8  	"github.com/status-im/status-go/protocol/communities/token"
     9  	"github.com/status-im/status-go/services/wallet/bigint"
    10  	"github.com/status-im/status-go/services/wallet/thirdparty"
    11  	"github.com/status-im/status-go/sqlite"
    12  )
    13  
    14  type CollectibleDataStorage interface {
    15  	SetData(collectibles []thirdparty.CollectibleData, allowUpdate bool) error
    16  	GetIDsNotInDB(ids []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleUniqueID, error)
    17  	GetData(ids []thirdparty.CollectibleUniqueID) (map[string]thirdparty.CollectibleData, error)
    18  	SetCommunityInfo(id thirdparty.CollectibleUniqueID, communityInfo thirdparty.CollectibleCommunityInfo) error
    19  	GetCommunityInfo(id thirdparty.CollectibleUniqueID) (*thirdparty.CollectibleCommunityInfo, error)
    20  }
    21  
    22  type CollectibleDataDB struct {
    23  	db *sql.DB
    24  }
    25  
    26  func NewCollectibleDataDB(sqlDb *sql.DB) *CollectibleDataDB {
    27  	return &CollectibleDataDB{
    28  		db: sqlDb,
    29  	}
    30  }
    31  
    32  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, soulbound"
    33  const collectibleCommunityDataColumns = "community_privileges_level"
    34  const collectibleTraitsColumns = "chain_id, contract_address, token_id, trait_type, trait_value, display_type, max_value"
    35  const selectCollectibleTraitsColumns = "trait_type, trait_value, display_type, max_value"
    36  
    37  func rowsToCollectibleTraits(rows *sql.Rows) ([]thirdparty.CollectibleTrait, error) {
    38  	var traits []thirdparty.CollectibleTrait = make([]thirdparty.CollectibleTrait, 0)
    39  	for rows.Next() {
    40  		var trait thirdparty.CollectibleTrait
    41  		err := rows.Scan(
    42  			&trait.TraitType,
    43  			&trait.Value,
    44  			&trait.DisplayType,
    45  			&trait.MaxValue,
    46  		)
    47  		if err != nil {
    48  			return nil, err
    49  		}
    50  		traits = append(traits, trait)
    51  	}
    52  	return traits, nil
    53  }
    54  
    55  func getCollectibleTraits(creator sqlite.StatementCreator, id thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleTrait, error) {
    56  	// Get traits list
    57  	selectTraits, err := creator.Prepare(fmt.Sprintf(`SELECT %s
    58  		FROM collectible_traits_cache
    59  		WHERE chain_id = ? AND contract_address = ? AND token_id = ?`, selectCollectibleTraitsColumns))
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  
    64  	rows, err := selectTraits.Query(
    65  		id.ContractID.ChainID,
    66  		id.ContractID.Address,
    67  		(*bigint.SQLBigIntBytes)(id.TokenID.Int),
    68  	)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	return rowsToCollectibleTraits(rows)
    74  }
    75  
    76  func upsertCollectibleTraits(creator sqlite.StatementCreator, id thirdparty.CollectibleUniqueID, traits []thirdparty.CollectibleTrait) error {
    77  	// Remove old traits list
    78  	deleteTraits, err := creator.Prepare(`DELETE FROM collectible_traits_cache WHERE chain_id = ? AND contract_address = ? AND token_id = ?`)
    79  	if err != nil {
    80  		return err
    81  	}
    82  
    83  	_, err = deleteTraits.Exec(
    84  		id.ContractID.ChainID,
    85  		id.ContractID.Address,
    86  		(*bigint.SQLBigIntBytes)(id.TokenID.Int),
    87  	)
    88  	if err != nil {
    89  		return err
    90  	}
    91  
    92  	// Insert new traits list
    93  	insertTrait, err := creator.Prepare(fmt.Sprintf(`INSERT INTO collectible_traits_cache (%s)
    94  																				VALUES (?, ?, ?, ?, ?, ?, ?)`, collectibleTraitsColumns))
    95  	if err != nil {
    96  		return err
    97  	}
    98  
    99  	for _, t := range traits {
   100  		_, err = insertTrait.Exec(
   101  			id.ContractID.ChainID,
   102  			id.ContractID.Address,
   103  			(*bigint.SQLBigIntBytes)(id.TokenID.Int),
   104  			t.TraitType,
   105  			t.Value,
   106  			t.DisplayType,
   107  			t.MaxValue,
   108  		)
   109  		if err != nil {
   110  			return err
   111  		}
   112  	}
   113  
   114  	return nil
   115  }
   116  
   117  func setCollectiblesData(creator sqlite.StatementCreator, collectibles []thirdparty.CollectibleData, allowUpdate bool) error {
   118  	insertCollectible, err := creator.Prepare(fmt.Sprintf(`%s INTO collectible_data_cache (%s) 
   119  																				VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, insertStatement(allowUpdate), collectibleDataColumns))
   120  	if err != nil {
   121  		return err
   122  	}
   123  
   124  	for _, c := range collectibles {
   125  		_, err = insertCollectible.Exec(
   126  			c.ID.ContractID.ChainID,
   127  			c.ID.ContractID.Address,
   128  			(*bigint.SQLBigIntBytes)(c.ID.TokenID.Int),
   129  			c.Provider,
   130  			c.Name,
   131  			c.Description,
   132  			c.Permalink,
   133  			c.ImageURL,
   134  			c.ImagePayload,
   135  			c.AnimationURL,
   136  			c.AnimationMediaType,
   137  			c.BackgroundColor,
   138  			c.TokenURI,
   139  			c.CommunityID,
   140  			c.Soulbound,
   141  		)
   142  		if err != nil {
   143  			return err
   144  		}
   145  
   146  		err = upsertContractType(creator, c.ID.ContractID, c.ContractType)
   147  		if err != nil {
   148  			return err
   149  		}
   150  
   151  		if allowUpdate {
   152  			err = upsertCollectibleTraits(creator, c.ID, c.Traits)
   153  			if err != nil {
   154  				return err
   155  			}
   156  		}
   157  	}
   158  
   159  	return nil
   160  }
   161  
   162  func (o *CollectibleDataDB) SetData(collectibles []thirdparty.CollectibleData, allowUpdate bool) (err error) {
   163  	tx, err := o.db.Begin()
   164  	if err != nil {
   165  		return err
   166  	}
   167  	defer func() {
   168  		if err == nil {
   169  			err = tx.Commit()
   170  			return
   171  		}
   172  		_ = tx.Rollback()
   173  	}()
   174  
   175  	// Insert new collectibles data
   176  	err = setCollectiblesData(tx, collectibles, allowUpdate)
   177  	if err != nil {
   178  		return err
   179  	}
   180  
   181  	return
   182  }
   183  
   184  func scanCollectiblesDataRow(row *sql.Row) (*thirdparty.CollectibleData, error) {
   185  	c := thirdparty.CollectibleData{
   186  		ID: thirdparty.CollectibleUniqueID{
   187  			TokenID: &bigint.BigInt{Int: big.NewInt(0)},
   188  		},
   189  		Traits: make([]thirdparty.CollectibleTrait, 0),
   190  	}
   191  	err := row.Scan(
   192  		&c.ID.ContractID.ChainID,
   193  		&c.ID.ContractID.Address,
   194  		(*bigint.SQLBigIntBytes)(c.ID.TokenID.Int),
   195  		&c.Provider,
   196  		&c.Name,
   197  		&c.Description,
   198  		&c.Permalink,
   199  		&c.ImageURL,
   200  		&c.ImagePayload,
   201  		&c.AnimationURL,
   202  		&c.AnimationMediaType,
   203  		&c.BackgroundColor,
   204  		&c.TokenURI,
   205  		&c.CommunityID,
   206  		&c.Soulbound,
   207  	)
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  	return &c, nil
   212  }
   213  
   214  func (o *CollectibleDataDB) GetIDsNotInDB(ids []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleUniqueID, error) {
   215  	ret := make([]thirdparty.CollectibleUniqueID, 0, len(ids))
   216  	idMap := make(map[string]thirdparty.CollectibleUniqueID, len(ids))
   217  
   218  	// Ensure we don't have duplicates
   219  	for _, id := range ids {
   220  		idMap[id.HashKey()] = id
   221  	}
   222  
   223  	exists, err := o.db.Prepare(`SELECT EXISTS (
   224  			SELECT 1 FROM collectible_data_cache
   225  			WHERE chain_id=? AND contract_address=? AND token_id=?
   226  		)`)
   227  	if err != nil {
   228  		return nil, err
   229  	}
   230  
   231  	for _, id := range idMap {
   232  		row := exists.QueryRow(
   233  			id.ContractID.ChainID,
   234  			id.ContractID.Address,
   235  			(*bigint.SQLBigIntBytes)(id.TokenID.Int),
   236  		)
   237  		var exists bool
   238  		err = row.Scan(&exists)
   239  		if err != nil {
   240  			return nil, err
   241  		}
   242  		if !exists {
   243  			ret = append(ret, id)
   244  		}
   245  	}
   246  
   247  	return ret, nil
   248  }
   249  
   250  func (o *CollectibleDataDB) GetData(ids []thirdparty.CollectibleUniqueID) (map[string]thirdparty.CollectibleData, error) {
   251  	ret := make(map[string]thirdparty.CollectibleData)
   252  
   253  	getData, err := o.db.Prepare(fmt.Sprintf(`SELECT %s
   254  		FROM collectible_data_cache
   255  		WHERE chain_id=? AND contract_address=? AND token_id=?`, collectibleDataColumns))
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  
   260  	for _, id := range ids {
   261  		row := getData.QueryRow(
   262  			id.ContractID.ChainID,
   263  			id.ContractID.Address,
   264  			(*bigint.SQLBigIntBytes)(id.TokenID.Int),
   265  		)
   266  		c, err := scanCollectiblesDataRow(row)
   267  		if err == sql.ErrNoRows {
   268  			continue
   269  		} else if err != nil {
   270  			return nil, err
   271  		} else {
   272  			// Get traits from different table
   273  			c.Traits, err = getCollectibleTraits(o.db, c.ID)
   274  			if err != nil {
   275  				return nil, err
   276  			}
   277  
   278  			// Get contract type from different table
   279  			c.ContractType, err = readContractType(o.db, c.ID.ContractID)
   280  			if err != nil {
   281  				return nil, err
   282  			}
   283  
   284  			ret[c.ID.HashKey()] = *c
   285  		}
   286  	}
   287  	return ret, nil
   288  }
   289  
   290  func (o *CollectibleDataDB) SetCommunityInfo(id thirdparty.CollectibleUniqueID, communityInfo thirdparty.CollectibleCommunityInfo) (err error) {
   291  	tx, err := o.db.Begin()
   292  	if err != nil {
   293  		return err
   294  	}
   295  	defer func() {
   296  		if err == nil {
   297  			err = tx.Commit()
   298  			return
   299  		}
   300  		_ = tx.Rollback()
   301  	}()
   302  
   303  	update, err := tx.Prepare(`UPDATE collectible_data_cache 
   304  		SET community_privileges_level=?
   305  		WHERE chain_id=? AND contract_address=? AND token_id=?`)
   306  	if err != nil {
   307  		return err
   308  	}
   309  
   310  	_, err = update.Exec(
   311  		communityInfo.PrivilegesLevel,
   312  		id.ContractID.ChainID,
   313  		id.ContractID.Address,
   314  		(*bigint.SQLBigIntBytes)(id.TokenID.Int),
   315  	)
   316  
   317  	return err
   318  }
   319  
   320  func (o *CollectibleDataDB) GetCommunityInfo(id thirdparty.CollectibleUniqueID) (*thirdparty.CollectibleCommunityInfo, error) {
   321  	ret := thirdparty.CollectibleCommunityInfo{
   322  		PrivilegesLevel: token.CommunityLevel,
   323  	}
   324  
   325  	getData, err := o.db.Prepare(fmt.Sprintf(`SELECT %s
   326  		FROM collectible_data_cache
   327  		WHERE chain_id=? AND contract_address=? AND token_id=?`, collectibleCommunityDataColumns))
   328  	if err != nil {
   329  		return nil, err
   330  	}
   331  
   332  	row := getData.QueryRow(
   333  		id.ContractID.ChainID,
   334  		id.ContractID.Address,
   335  		(*bigint.SQLBigIntBytes)(id.TokenID.Int),
   336  	)
   337  
   338  	var dbPrivilegesLevel sql.NullByte
   339  
   340  	err = row.Scan(
   341  		&dbPrivilegesLevel,
   342  	)
   343  
   344  	if err == sql.ErrNoRows {
   345  		return nil, nil
   346  	} else if err != nil {
   347  		return nil, err
   348  	}
   349  
   350  	if dbPrivilegesLevel.Valid {
   351  		ret.PrivilegesLevel = token.PrivilegesLevel(dbPrivilegesLevel.Byte)
   352  	}
   353  
   354  	return &ret, nil
   355  }