github.com/status-im/status-go@v1.1.0/services/wallet/collectibles/ownership_db.go (about) 1 package collectibles 2 3 import ( 4 "database/sql" 5 "fmt" 6 "math/big" 7 "sync" 8 9 "github.com/ethereum/go-ethereum/common" 10 11 "github.com/jmoiron/sqlx" 12 13 "github.com/status-im/status-go/services/wallet/bigint" 14 w_common "github.com/status-im/status-go/services/wallet/common" 15 "github.com/status-im/status-go/services/wallet/thirdparty" 16 "github.com/status-im/status-go/sqlite" 17 ) 18 19 const InvalidTimestamp = int64(-1) 20 21 type OwnershipDB struct { 22 db *sql.DB 23 mu sync.Mutex 24 } 25 26 func NewOwnershipDB(sqlDb *sql.DB) *OwnershipDB { 27 return &OwnershipDB{ 28 db: sqlDb, 29 } 30 } 31 32 const selectOwnershipColumns = "chain_id, contract_address, token_id" 33 34 const collectiblesOwnershipColumns = "token_id, owner_address, balance" 35 36 const ownershipTimestampColumns = "owner_address, chain_id, timestamp" 37 const selectOwnershipTimestampColumns = "timestamp" 38 39 func insertTmpOwnership( 40 db *sql.DB, 41 chainID w_common.ChainID, 42 ownerAddress common.Address, 43 balancesPerContractAdddress thirdparty.TokenBalancesPerContractAddress, 44 ) error { 45 // Put old/new ownership data into temp tables 46 // NOTE: Temp table CREATE doesn't work with prepared statements, 47 // so we have to use Exec directly 48 _, err := db.Exec(` 49 DROP TABLE IF EXISTS temp.old_collectibles_ownership_cache; 50 CREATE TABLE temp.old_collectibles_ownership_cache( 51 contract_address VARCHAR NOT NULL, 52 token_id BLOB NOT NULL, 53 balance BLOB NOT NULL 54 ); 55 DROP TABLE IF EXISTS temp.new_collectibles_ownership_cache; 56 CREATE TABLE temp.new_collectibles_ownership_cache( 57 contract_address VARCHAR NOT NULL, 58 token_id BLOB NOT NULL, 59 balance BLOB NOT NULL 60 );`) 61 if err != nil { 62 return err 63 } 64 65 insertTmpOldOwnership, err := db.Prepare(` 66 INSERT INTO temp.old_collectibles_ownership_cache 67 SELECT contract_address, token_id, balance FROM collectibles_ownership_cache 68 WHERE chain_id = ? AND owner_address = ?`) 69 if err != nil { 70 return err 71 } 72 defer insertTmpOldOwnership.Close() 73 74 _, err = insertTmpOldOwnership.Exec(chainID, ownerAddress) 75 if err != nil { 76 return err 77 } 78 79 insertTmpNewOwnership, err := db.Prepare(` 80 INSERT INTO temp.new_collectibles_ownership_cache (contract_address, token_id, balance) 81 VALUES (?, ?, ?)`) 82 if err != nil { 83 return err 84 } 85 defer insertTmpNewOwnership.Close() 86 87 for contractAddress, balances := range balancesPerContractAdddress { 88 for _, balance := range balances { 89 _, err = insertTmpNewOwnership.Exec( 90 contractAddress, 91 (*bigint.SQLBigIntBytes)(balance.TokenID.Int), 92 (*bigint.SQLBigIntBytes)(balance.Balance.Int), 93 ) 94 if err != nil { 95 return err 96 } 97 } 98 } 99 100 return nil 101 } 102 103 func removeOldAddressOwnership( 104 creator sqlite.StatementCreator, 105 chainID w_common.ChainID, 106 ownerAddress common.Address, 107 ) ([]thirdparty.CollectibleUniqueID, error) { 108 // Find collectibles in the DB that are not in the temp table 109 removedQuery, err := creator.Prepare(fmt.Sprintf(` 110 SELECT %d, tOld.contract_address, tOld.token_id 111 FROM temp.old_collectibles_ownership_cache tOld 112 LEFT JOIN temp.new_collectibles_ownership_cache tNew ON 113 tOld.contract_address = tNew.contract_address AND tOld.token_id = tNew.token_id 114 WHERE 115 tNew.contract_address IS NULL 116 `, chainID)) 117 if err != nil { 118 return nil, err 119 } 120 defer removedQuery.Close() 121 122 removedRows, err := removedQuery.Query() 123 if err != nil { 124 return nil, err 125 } 126 127 defer removedRows.Close() 128 removedIDs, err := thirdparty.RowsToCollectibles(removedRows) 129 if err != nil { 130 return nil, err 131 } 132 133 removeOwnership, err := creator.Prepare("DELETE FROM collectibles_ownership_cache WHERE chain_id = ? AND owner_address = ? AND contract_address = ? AND token_id = ?") 134 if err != nil { 135 return nil, err 136 } 137 defer removeOwnership.Close() 138 139 for _, id := range removedIDs { 140 _, err = removeOwnership.Exec( 141 chainID, 142 ownerAddress, 143 id.ContractID.Address, 144 (*bigint.SQLBigIntBytes)(id.TokenID.Int), 145 ) 146 if err != nil { 147 return nil, err 148 } 149 } 150 151 return removedIDs, nil 152 } 153 154 func updateChangedAddressOwnership( 155 creator sqlite.StatementCreator, 156 chainID w_common.ChainID, 157 ownerAddress common.Address, 158 ) ([]thirdparty.CollectibleUniqueID, error) { 159 // Find collectibles in the temp table that are in the DB and have a different balance 160 updatedQuery, err := creator.Prepare(fmt.Sprintf(` 161 SELECT %d, tNew.contract_address, tNew.token_id 162 FROM temp.new_collectibles_ownership_cache tNew 163 LEFT JOIN temp.old_collectibles_ownership_cache tOld ON 164 tOld.contract_address = tNew.contract_address AND tOld.token_id = tNew.token_id 165 WHERE 166 tOld.contract_address IS NOT NULL AND tOld.balance != tNew.balance 167 `, chainID)) 168 if err != nil { 169 return nil, err 170 } 171 defer updatedQuery.Close() 172 173 updatedRows, err := updatedQuery.Query() 174 if err != nil { 175 return nil, err 176 } 177 defer updatedRows.Close() 178 179 updatedIDs, err := thirdparty.RowsToCollectibles(updatedRows) 180 if err != nil { 181 return nil, err 182 } 183 184 updateOwnership, err := creator.Prepare(` 185 UPDATE collectibles_ownership_cache 186 SET balance = (SELECT tNew.balance 187 FROM temp.new_collectibles_ownership_cache tNew 188 WHERE tNew.contract_address = collectibles_ownership_cache.contract_address AND tNew.token_id = collectibles_ownership_cache.token_id) 189 WHERE chain_id = ? AND owner_address = ? AND contract_address = ? AND token_id = ? 190 `) 191 if err != nil { 192 return nil, err 193 } 194 defer updateOwnership.Close() 195 196 for _, id := range updatedIDs { 197 _, err = updateOwnership.Exec( 198 chainID, 199 ownerAddress, 200 id.ContractID.Address, 201 (*bigint.SQLBigIntBytes)(id.TokenID.Int)) 202 if err != nil { 203 return nil, err 204 } 205 } 206 207 return updatedIDs, nil 208 } 209 210 func insertNewAddressOwnership( 211 creator sqlite.StatementCreator, 212 chainID w_common.ChainID, 213 ownerAddress common.Address, 214 ) ([]thirdparty.CollectibleUniqueID, error) { 215 // Find collectibles in the temp table that are not in the DB 216 insertedQuery, err := creator.Prepare(fmt.Sprintf(` 217 SELECT %d, tNew.contract_address, tNew.token_id 218 FROM temp.new_collectibles_ownership_cache tNew 219 LEFT JOIN temp.old_collectibles_ownership_cache tOld ON 220 tOld.contract_address = tNew.contract_address AND tOld.token_id = tNew.token_id 221 WHERE 222 tOld.contract_address IS NULL 223 `, chainID)) 224 if err != nil { 225 return nil, err 226 } 227 defer insertedQuery.Close() 228 229 insertedRows, err := insertedQuery.Query() 230 if err != nil { 231 return nil, err 232 } 233 defer insertedRows.Close() 234 235 insertedIDs, err := thirdparty.RowsToCollectibles(insertedRows) 236 if err != nil { 237 return nil, err 238 } 239 240 insertOwnership, err := creator.Prepare(fmt.Sprintf(` 241 INSERT INTO collectibles_ownership_cache 242 SELECT 243 %d, tNew.contract_address, tNew.token_id, X'%s', tNew.balance, NULL 244 FROM temp.new_collectibles_ownership_cache tNew 245 WHERE 246 tNew.contract_address = ? AND tNew.token_id = ? 247 `, chainID, ownerAddress.Hex()[2:])) 248 if err != nil { 249 return nil, err 250 } 251 defer insertOwnership.Close() 252 253 for _, id := range insertedIDs { 254 _, err = insertOwnership.Exec( 255 id.ContractID.Address, 256 (*bigint.SQLBigIntBytes)(id.TokenID.Int)) 257 if err != nil { 258 return nil, err 259 } 260 } 261 262 return insertedIDs, nil 263 } 264 265 func updateAddressOwnership( 266 tx sqlite.StatementCreator, 267 chainID w_common.ChainID, 268 ownerAddress common.Address, 269 ) (removedIDs, updatedIDs, insertedIDs []thirdparty.CollectibleUniqueID, err error) { 270 removedIDs, err = removeOldAddressOwnership(tx, chainID, ownerAddress) 271 if err != nil { 272 return 273 } 274 275 updatedIDs, err = updateChangedAddressOwnership(tx, chainID, ownerAddress) 276 if err != nil { 277 return 278 } 279 280 insertedIDs, err = insertNewAddressOwnership(tx, chainID, ownerAddress) 281 if err != nil { 282 return 283 } 284 285 return 286 } 287 288 func updateAddressOwnershipTimestamp(creator sqlite.StatementCreator, ownerAddress common.Address, chainID w_common.ChainID, timestamp int64) error { 289 updateTimestamp, err := creator.Prepare(fmt.Sprintf(`INSERT OR REPLACE INTO collectibles_ownership_update_timestamps (%s) 290 VALUES (?, ?, ?)`, ownershipTimestampColumns)) 291 if err != nil { 292 return err 293 } 294 defer updateTimestamp.Close() 295 296 _, err = updateTimestamp.Exec(ownerAddress, chainID, timestamp) 297 298 return err 299 } 300 301 // Returns the list of added/removed IDs when comparing the given list of IDs with the ones in the DB. 302 // Call before Update for the result to be useful. 303 func (o *OwnershipDB) GetIDsNotInDB( 304 ownerAddress common.Address, 305 newIDs []thirdparty.CollectibleUniqueID) ([]thirdparty.CollectibleUniqueID, error) { 306 ret := make([]thirdparty.CollectibleUniqueID, 0, len(newIDs)) 307 308 exists, err := o.db.Prepare(`SELECT EXISTS ( 309 SELECT 1 FROM collectibles_ownership_cache 310 WHERE chain_id=? AND contract_address=? AND token_id=? AND owner_address=? 311 )`) 312 if err != nil { 313 return nil, err 314 } 315 316 for _, id := range newIDs { 317 row := exists.QueryRow( 318 id.ContractID.ChainID, 319 id.ContractID.Address, 320 (*bigint.SQLBigIntBytes)(id.TokenID.Int), 321 ownerAddress, 322 ) 323 var exists bool 324 err = row.Scan(&exists) 325 if err != nil { 326 return nil, err 327 } 328 if !exists { 329 ret = append(ret, id) 330 } 331 } 332 333 return ret, nil 334 } 335 336 func (o *OwnershipDB) GetIsFirstOfCollection(onwerAddress common.Address, newIDs []thirdparty.CollectibleUniqueID) (map[thirdparty.CollectibleUniqueID]bool, error) { 337 ret := make(map[thirdparty.CollectibleUniqueID]bool) 338 339 exists, err := o.db.Prepare(`SELECT count(*) FROM collectibles_ownership_cache 340 WHERE chain_id=? AND contract_address=? AND owner_address=?`) 341 if err != nil { 342 return nil, err 343 } 344 345 for _, id := range newIDs { 346 row := exists.QueryRow( 347 id.ContractID.ChainID, 348 id.ContractID.Address, 349 onwerAddress, 350 ) 351 var count int 352 err = row.Scan(&count) 353 if err != nil { 354 return nil, err 355 } 356 ret[id] = count <= 1 357 } 358 return ret, nil 359 } 360 361 func (o *OwnershipDB) Update(chainID w_common.ChainID, ownerAddress common.Address, balances thirdparty.TokenBalancesPerContractAddress, timestamp int64) (removedIDs, updatedIDs, insertedIDs []thirdparty.CollectibleUniqueID, err error) { 362 // Ensure all steps are done atomically 363 o.mu.Lock() 364 defer o.mu.Unlock() 365 366 err = insertTmpOwnership(o.db, chainID, ownerAddress, balances) 367 if err != nil { 368 return 369 } 370 371 var ( 372 tx *sql.Tx 373 ) 374 tx, err = o.db.Begin() 375 if err != nil { 376 return 377 } 378 defer func() { 379 if err == nil { 380 err = tx.Commit() 381 return 382 } 383 _ = tx.Rollback() 384 }() 385 386 // Compare tmp and current ownership tables and update the current one 387 removedIDs, updatedIDs, insertedIDs, err = updateAddressOwnership(tx, chainID, ownerAddress) 388 if err != nil { 389 return 390 } 391 392 // Update timestamp 393 err = updateAddressOwnershipTimestamp(tx, ownerAddress, chainID, timestamp) 394 395 return 396 } 397 398 func (o *OwnershipDB) GetOwnedCollectibles(chainIDs []w_common.ChainID, ownerAddresses []common.Address, offset int, limit int) ([]thirdparty.CollectibleUniqueID, error) { 399 query, args, err := sqlx.In(fmt.Sprintf(`SELECT DISTINCT %s 400 FROM collectibles_ownership_cache 401 WHERE chain_id IN (?) AND owner_address IN (?) 402 LIMIT ? OFFSET ?`, selectOwnershipColumns), chainIDs, ownerAddresses, limit, offset) 403 if err != nil { 404 return nil, err 405 } 406 407 stmt, err := o.db.Prepare(query) 408 if err != nil { 409 return nil, err 410 } 411 defer stmt.Close() 412 413 rows, err := stmt.Query(args...) 414 if err != nil { 415 return nil, err 416 } 417 defer rows.Close() 418 419 return thirdparty.RowsToCollectibles(rows) 420 } 421 422 func (o *OwnershipDB) FetchCachedCollectibleOwnersByContractAddress(chainID w_common.ChainID, contractAddress common.Address) (*thirdparty.CollectibleContractOwnership, error) { 423 query, args, err := sqlx.In(fmt.Sprintf(`SELECT %s 424 FROM collectibles_ownership_cache 425 WHERE chain_id = ? AND contract_address = ?`, collectiblesOwnershipColumns), chainID, contractAddress) 426 if err != nil { 427 return nil, err 428 } 429 430 var ret thirdparty.CollectibleContractOwnership 431 432 stmt, err := o.db.Prepare(query) 433 if err != nil { 434 return nil, err 435 } 436 defer stmt.Close() 437 438 rows, err := stmt.Query(args...) 439 if err != nil { 440 return nil, err 441 } 442 defer rows.Close() 443 444 tokenID := &bigint.BigInt{Int: big.NewInt(0)} 445 var ownerAddress common.Address 446 balance := &bigint.BigInt{Int: big.NewInt(0)} 447 var tokenBalances []thirdparty.TokenBalance 448 449 for rows.Next() { 450 err = rows.Scan( 451 (*bigint.SQLBigIntBytes)(tokenID.Int), 452 &ownerAddress, 453 (*bigint.SQLBigIntBytes)(balance.Int), 454 ) 455 if err != nil { 456 return nil, err 457 } 458 459 tokenBalance := thirdparty.TokenBalance{ 460 TokenID: tokenID, 461 Balance: balance, 462 } 463 tokenBalances = append(tokenBalances, tokenBalance) 464 465 collectibleOwner := thirdparty.CollectibleOwner{ 466 OwnerAddress: ownerAddress, 467 TokenBalances: tokenBalances, 468 } 469 470 ret.ContractAddress = contractAddress 471 ret.Owners = append(ret.Owners, collectibleOwner) 472 } 473 474 return &ret, nil 475 } 476 477 func (o *OwnershipDB) GetOwnedCollectible(chainID w_common.ChainID, ownerAddresses common.Address, contractAddress common.Address, tokenID *big.Int) (*thirdparty.CollectibleUniqueID, error) { 478 query := fmt.Sprintf(`SELECT %s 479 FROM collectibles_ownership_cache 480 WHERE chain_id = ? AND owner_address = ? AND contract_address = ? AND token_id = ?`, selectOwnershipColumns) 481 482 stmt, err := o.db.Prepare(query) 483 if err != nil { 484 return nil, err 485 } 486 defer stmt.Close() 487 488 rows, err := stmt.Query(chainID, ownerAddresses, contractAddress, (*bigint.SQLBigIntBytes)(tokenID)) 489 if err != nil { 490 return nil, err 491 } 492 defer rows.Close() 493 494 ids, err := thirdparty.RowsToCollectibles(rows) 495 if err != nil { 496 return nil, err 497 } 498 499 if len(ids) == 0 { 500 return nil, nil 501 } 502 503 return &ids[0], nil 504 } 505 506 func (o *OwnershipDB) GetOwnershipUpdateTimestamp(owner common.Address, chainID w_common.ChainID) (int64, error) { 507 query := fmt.Sprintf(`SELECT %s 508 FROM collectibles_ownership_update_timestamps 509 WHERE owner_address = ? AND chain_id = ?`, selectOwnershipTimestampColumns) 510 511 stmt, err := o.db.Prepare(query) 512 if err != nil { 513 return InvalidTimestamp, err 514 } 515 defer stmt.Close() 516 517 row := stmt.QueryRow(owner, chainID) 518 519 var timestamp int64 520 521 err = row.Scan(×tamp) 522 523 if err == sql.ErrNoRows { 524 return InvalidTimestamp, nil 525 } else if err != nil { 526 return InvalidTimestamp, err 527 } 528 529 return timestamp, nil 530 } 531 532 func (o *OwnershipDB) GetLatestOwnershipUpdateTimestamp(chainID w_common.ChainID) (int64, error) { 533 query := `SELECT MAX(timestamp) 534 FROM collectibles_ownership_update_timestamps 535 WHERE chain_id = ?` 536 537 stmt, err := o.db.Prepare(query) 538 if err != nil { 539 return InvalidTimestamp, err 540 } 541 defer stmt.Close() 542 543 row := stmt.QueryRow(chainID) 544 545 var timestamp sql.NullInt64 546 547 err = row.Scan(×tamp) 548 549 if err != nil { 550 return InvalidTimestamp, err 551 } 552 if timestamp.Valid { 553 return timestamp.Int64, nil 554 } 555 556 return InvalidTimestamp, nil 557 } 558 559 func (o *OwnershipDB) GetOwnership(id thirdparty.CollectibleUniqueID) ([]thirdparty.AccountBalance, error) { 560 query := fmt.Sprintf(`SELECT c.owner_address, c.balance, COALESCE(t.timestamp, %d) 561 FROM collectibles_ownership_cache c 562 LEFT JOIN transfers t ON 563 c.transfer_id = t.hash 564 WHERE 565 c.chain_id = ? AND c.contract_address = ? AND c.token_id = ?`, InvalidTimestamp) 566 567 stmt, err := o.db.Prepare(query) 568 if err != nil { 569 return nil, err 570 } 571 defer stmt.Close() 572 573 rows, err := stmt.Query(id.ContractID.ChainID, id.ContractID.Address, (*bigint.SQLBigIntBytes)(id.TokenID.Int)) 574 if err != nil { 575 return nil, err 576 } 577 defer rows.Close() 578 579 var ret []thirdparty.AccountBalance 580 for rows.Next() { 581 accountBalance := thirdparty.AccountBalance{ 582 Balance: &bigint.BigInt{Int: big.NewInt(0)}, 583 } 584 err = rows.Scan( 585 &accountBalance.Address, 586 (*bigint.SQLBigIntBytes)(accountBalance.Balance.Int), 587 &accountBalance.TxTimestamp, 588 ) 589 if err != nil { 590 return nil, err 591 } 592 593 ret = append(ret, accountBalance) 594 } 595 596 return ret, nil 597 } 598 599 func (o *OwnershipDB) SetTransferID(ownerAddress common.Address, id thirdparty.CollectibleUniqueID, transferID common.Hash) (bool, error) { 600 query := `UPDATE collectibles_ownership_cache 601 SET transfer_id = ? 602 WHERE chain_id = ? AND contract_address = ? AND token_id = ? AND owner_address = ?` 603 604 stmt, err := o.db.Prepare(query) 605 if err != nil { 606 return false, err 607 } 608 defer stmt.Close() 609 610 res, err := stmt.Exec(transferID, id.ContractID.ChainID, id.ContractID.Address, (*bigint.SQLBigIntBytes)(id.TokenID.Int), ownerAddress) 611 if err != nil { 612 return false, err 613 } 614 615 rowsAffected, err := res.RowsAffected() 616 if err != nil { 617 return false, err 618 } 619 620 if rowsAffected > 0 { 621 return true, nil 622 } 623 624 return false, nil 625 } 626 627 func (o *OwnershipDB) GetTransferID(ownerAddress common.Address, id thirdparty.CollectibleUniqueID) (*common.Hash, error) { 628 query := `SELECT transfer_id 629 FROM collectibles_ownership_cache 630 WHERE chain_id = ? AND contract_address = ? AND token_id = ? AND owner_address = ? 631 LIMIT 1` 632 633 stmt, err := o.db.Prepare(query) 634 if err != nil { 635 return nil, err 636 } 637 defer stmt.Close() 638 639 row := stmt.QueryRow(id.ContractID.ChainID, id.ContractID.Address, (*bigint.SQLBigIntBytes)(id.TokenID.Int), ownerAddress) 640 641 var dbTransferID []byte 642 643 err = row.Scan(&dbTransferID) 644 645 if err == sql.ErrNoRows { 646 return nil, nil 647 } else if err != nil { 648 return nil, err 649 } 650 651 if len(dbTransferID) > 0 { 652 transferID := common.BytesToHash(dbTransferID) 653 return &transferID, nil 654 } 655 656 return nil, nil 657 } 658 659 func (o *OwnershipDB) GetCollectiblesWithNoTransferID(account common.Address, chainID w_common.ChainID) ([]thirdparty.CollectibleUniqueID, error) { 660 query := `SELECT contract_address, token_id 661 FROM collectibles_ownership_cache 662 WHERE chain_id = ? AND owner_address = ? AND transfer_id IS NULL` 663 664 stmt, err := o.db.Prepare(query) 665 if err != nil { 666 return nil, err 667 } 668 defer stmt.Close() 669 670 rows, err := stmt.Query(chainID, account) 671 if err != nil { 672 return nil, err 673 } 674 defer rows.Close() 675 676 var ret []thirdparty.CollectibleUniqueID 677 for rows.Next() { 678 id := thirdparty.CollectibleUniqueID{ 679 ContractID: thirdparty.ContractID{ 680 ChainID: chainID, 681 }, 682 TokenID: &bigint.BigInt{Int: big.NewInt(0)}, 683 } 684 err = rows.Scan( 685 &id.ContractID.Address, 686 (*bigint.SQLBigIntBytes)(id.TokenID.Int), 687 ) 688 if err != nil { 689 return nil, err 690 } 691 692 ret = append(ret, id) 693 } 694 695 return ret, nil 696 }