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 }