github.com/status-im/status-go@v1.1.0/services/wallet/transfer/database.go (about) 1 package transfer 2 3 import ( 4 "context" 5 "database/sql" 6 "database/sql/driver" 7 "encoding/json" 8 "errors" 9 "math/big" 10 "reflect" 11 12 "github.com/ethereum/go-ethereum/common" 13 "github.com/ethereum/go-ethereum/core/types" 14 "github.com/ethereum/go-ethereum/log" 15 16 "github.com/status-im/status-go/services/wallet/bigint" 17 w_common "github.com/status-im/status-go/services/wallet/common" 18 "github.com/status-im/status-go/services/wallet/thirdparty" 19 "github.com/status-im/status-go/sqlite" 20 ) 21 22 // DBHeader fields from header that are stored in database. 23 type DBHeader struct { 24 Number *big.Int 25 Hash common.Hash 26 Timestamp uint64 27 PreloadedTransactions []*PreloadedTransaction 28 Network uint64 29 Address common.Address 30 // Head is true if the block was a head at the time it was pulled from chain. 31 Head bool 32 // Loaded is true if transfers from this block have been already fetched 33 Loaded bool 34 } 35 36 func toDBHeader(header *types.Header, blockHash common.Hash, account common.Address) *DBHeader { 37 return &DBHeader{ 38 Hash: blockHash, 39 Number: header.Number, 40 Timestamp: header.Time, 41 Loaded: false, 42 Address: account, 43 } 44 } 45 46 // SyncOption is used to specify that application processed transfers for that block. 47 type SyncOption uint 48 49 // JSONBlob type for marshaling/unmarshaling inner type to json. 50 type JSONBlob struct { 51 data interface{} 52 } 53 54 // Scan implements interface. 55 func (blob *JSONBlob) Scan(value interface{}) error { 56 if value == nil || reflect.ValueOf(blob.data).IsNil() { 57 return nil 58 } 59 bytes, ok := value.([]byte) 60 if !ok { 61 return errors.New("not a byte slice") 62 } 63 if len(bytes) == 0 { 64 return nil 65 } 66 err := json.Unmarshal(bytes, blob.data) 67 return err 68 } 69 70 // Value implements interface. 71 func (blob *JSONBlob) Value() (driver.Value, error) { 72 if blob.data == nil || reflect.ValueOf(blob.data).IsNil() { 73 return nil, nil 74 } 75 return json.Marshal(blob.data) 76 } 77 78 func NewDB(client *sql.DB) *Database { 79 return &Database{client: client} 80 } 81 82 // Database sql wrapper for operations with wallet objects. 83 type Database struct { 84 client *sql.DB 85 } 86 87 // Close closes database. 88 func (db *Database) Close() error { 89 return db.client.Close() 90 } 91 92 func (db *Database) SaveBlocks(chainID uint64, headers []*DBHeader) (err error) { 93 var ( 94 tx *sql.Tx 95 ) 96 tx, err = db.client.Begin() 97 if err != nil { 98 return err 99 } 100 defer func() { 101 if err == nil { 102 err = tx.Commit() 103 return 104 } 105 _ = tx.Rollback() 106 }() 107 108 err = insertBlocksWithTransactions(chainID, tx, headers) 109 if err != nil { 110 return 111 } 112 113 return 114 } 115 116 func saveTransfersMarkBlocksLoaded(creator statementCreator, chainID uint64, address common.Address, transfers []Transfer, blocks []*big.Int) (err error) { 117 err = updateOrInsertTransfers(chainID, creator, transfers) 118 if err != nil { 119 return 120 } 121 122 err = markBlocksAsLoaded(chainID, creator, address, blocks) 123 if err != nil { 124 return 125 } 126 127 return 128 } 129 130 // GetTransfersInRange loads transfers for a given address between two blocks. 131 func (db *Database) GetTransfersInRange(chainID uint64, address common.Address, start, end *big.Int) (rst []Transfer, err error) { 132 query := newTransfersQuery().FilterNetwork(chainID).FilterAddress(address).FilterStart(start).FilterEnd(end).FilterLoaded(1) 133 rows, err := db.client.Query(query.String(), query.Args()...) 134 if err != nil { 135 return 136 } 137 defer rows.Close() 138 return query.TransferScan(rows) 139 } 140 141 // GetTransfersByAddress loads transfers for a given address between two blocks. 142 func (db *Database) GetTransfersByAddress(chainID uint64, address common.Address, toBlock *big.Int, limit int64) (rst []Transfer, err error) { 143 query := newTransfersQuery(). 144 FilterNetwork(chainID). 145 FilterAddress(address). 146 FilterEnd(toBlock). 147 FilterLoaded(1). 148 SortByBlockNumberAndHash(). 149 Limit(limit) 150 151 rows, err := db.client.Query(query.String(), query.Args()...) 152 if err != nil { 153 return 154 } 155 defer rows.Close() 156 return query.TransferScan(rows) 157 } 158 159 // GetTransfersByAddressAndBlock loads transfers for a given address and block. 160 func (db *Database) GetTransfersByAddressAndBlock(chainID uint64, address common.Address, block *big.Int, limit int64) (rst []Transfer, err error) { 161 query := newTransfersQuery(). 162 FilterNetwork(chainID). 163 FilterAddress(address). 164 FilterBlockNumber(block). 165 FilterLoaded(1). 166 SortByBlockNumberAndHash(). 167 Limit(limit) 168 169 rows, err := db.client.Query(query.String(), query.Args()...) 170 if err != nil { 171 return 172 } 173 defer rows.Close() 174 return query.TransferScan(rows) 175 } 176 177 // GetTransfers load transfers transfer between two blocks. 178 func (db *Database) GetTransfers(chainID uint64, start, end *big.Int) (rst []Transfer, err error) { 179 query := newTransfersQuery().FilterNetwork(chainID).FilterStart(start).FilterEnd(end).FilterLoaded(1) 180 rows, err := db.client.Query(query.String(), query.Args()...) 181 if err != nil { 182 return 183 } 184 defer rows.Close() 185 return query.TransferScan(rows) 186 } 187 188 func (db *Database) GetTransfersForIdentities(ctx context.Context, identities []TransactionIdentity) (rst []Transfer, err error) { 189 query := newTransfersQuery() 190 for _, identity := range identities { 191 subQuery := newSubQuery() 192 subQuery = subQuery.FilterNetwork(uint64(identity.ChainID)).FilterTransactionID(identity.Hash).FilterAddress(identity.Address) 193 query.addSubQuery(subQuery, OrSeparator) 194 } 195 rows, err := db.client.QueryContext(ctx, query.String(), query.Args()...) 196 if err != nil { 197 return 198 } 199 defer rows.Close() 200 return query.TransferScan(rows) 201 } 202 203 func (db *Database) GetTransactionsToLoad(chainID uint64, address common.Address, blockNumber *big.Int) (rst []*PreloadedTransaction, err error) { 204 query := newTransfersQueryForPreloadedTransactions(). 205 FilterNetwork(chainID). 206 FilterLoaded(0) 207 208 if address != (common.Address{}) { 209 query.FilterAddress(address) 210 } 211 212 if blockNumber != nil { 213 query.FilterBlockNumber(blockNumber) 214 } 215 216 rows, err := db.client.Query(query.String(), query.Args()...) 217 if err != nil { 218 return 219 } 220 defer rows.Close() 221 return query.PreloadedTransactionScan(rows) 222 } 223 224 // statementCreator allows to pass transaction or database to use in consumer. 225 type statementCreator interface { 226 Prepare(query string) (*sql.Stmt, error) 227 } 228 229 // Only used by status-mobile 230 func (db *Database) InsertBlock(chainID uint64, account common.Address, blockNumber *big.Int, blockHash common.Hash) error { 231 var ( 232 tx *sql.Tx 233 ) 234 tx, err := db.client.Begin() 235 if err != nil { 236 return err 237 } 238 defer func() { 239 if err == nil { 240 err = tx.Commit() 241 return 242 } 243 _ = tx.Rollback() 244 }() 245 246 blockDB := blockDBFields{ 247 chainID: chainID, 248 account: account, 249 blockNumber: blockNumber, 250 blockHash: blockHash, 251 } 252 return insertBlockDBFields(tx, blockDB) 253 } 254 255 type blockDBFields struct { 256 chainID uint64 257 account common.Address 258 blockNumber *big.Int 259 blockHash common.Hash 260 } 261 262 func insertBlockDBFields(creator statementCreator, block blockDBFields) error { 263 insert, err := creator.Prepare("INSERT OR IGNORE INTO blocks(network_id, address, blk_number, blk_hash, loaded) VALUES (?, ?, ?, ?, ?)") 264 if err != nil { 265 return err 266 } 267 268 _, err = insert.Exec(block.chainID, block.account, (*bigint.SQLBigInt)(block.blockNumber), block.blockHash, true) 269 return err 270 } 271 272 func insertBlocksWithTransactions(chainID uint64, creator statementCreator, headers []*DBHeader) error { 273 insert, err := creator.Prepare("INSERT OR IGNORE INTO blocks(network_id, address, blk_number, blk_hash, loaded) VALUES (?, ?, ?, ?, ?)") 274 if err != nil { 275 return err 276 } 277 updateTx, err := creator.Prepare(`UPDATE transfers 278 SET log = ?, log_index = ? 279 WHERE network_id = ? AND address = ? AND hash = ?`) 280 if err != nil { 281 return err 282 } 283 284 insertTx, err := creator.Prepare(`INSERT OR IGNORE 285 INTO transfers (network_id, address, sender, hash, blk_number, blk_hash, type, timestamp, log, loaded, log_index, token_id, amount_padded128hex) 286 VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, 0, ?, ?, ?)`) 287 if err != nil { 288 return err 289 } 290 291 for _, header := range headers { 292 _, err = insert.Exec(chainID, header.Address, (*bigint.SQLBigInt)(header.Number), header.Hash, header.Loaded) 293 if err != nil { 294 return err 295 } 296 for _, transaction := range header.PreloadedTransactions { 297 var logIndex *uint 298 if transaction.Log != nil { 299 logIndex = new(uint) 300 *logIndex = transaction.Log.Index 301 } 302 res, err := updateTx.Exec(&JSONBlob{transaction.Log}, logIndex, chainID, header.Address, transaction.ID) 303 if err != nil { 304 return err 305 } 306 affected, err := res.RowsAffected() 307 if err != nil { 308 return err 309 } 310 if affected > 0 { 311 continue 312 } 313 314 tokenID := (*bigint.SQLBigIntBytes)(transaction.TokenID) 315 txValue := sqlite.BigIntToPadded128BitsStr(transaction.Value) 316 // Is that correct to set sender as account address? 317 _, err = insertTx.Exec(chainID, header.Address, header.Address, transaction.ID, (*bigint.SQLBigInt)(header.Number), header.Hash, transaction.Type, &JSONBlob{transaction.Log}, logIndex, tokenID, txValue) 318 if err != nil { 319 log.Error("error saving token transfer", "err", err) 320 return err 321 } 322 } 323 } 324 return nil 325 } 326 327 func updateOrInsertTransfers(chainID uint64, creator statementCreator, transfers []Transfer) error { 328 txsDBFields := make([]transferDBFields, 0, len(transfers)) 329 for _, localTransfer := range transfers { 330 // to satisfy gosec: C601 checks 331 t := localTransfer 332 var receiptType *uint8 333 var txHash, blockHash *common.Hash 334 var receiptStatus, cumulativeGasUsed, gasUsed *uint64 335 var contractAddress *common.Address 336 var transactionIndex, logIndex *uint 337 338 if t.Receipt != nil { 339 receiptType = &t.Receipt.Type 340 receiptStatus = &t.Receipt.Status 341 txHash = &t.Receipt.TxHash 342 if t.Log != nil { 343 logIndex = new(uint) 344 *logIndex = t.Log.Index 345 } 346 blockHash = &t.Receipt.BlockHash 347 cumulativeGasUsed = &t.Receipt.CumulativeGasUsed 348 contractAddress = &t.Receipt.ContractAddress 349 gasUsed = &t.Receipt.GasUsed 350 transactionIndex = &t.Receipt.TransactionIndex 351 } 352 353 var txProtected *bool 354 var txGas, txNonce, txSize *uint64 355 var txGasPrice, txGasTipCap, txGasFeeCap *big.Int 356 var txType *uint8 357 var txValue *big.Int 358 var tokenAddress *common.Address 359 var tokenID *big.Int 360 var txFrom *common.Address 361 var txTo *common.Address 362 if t.Transaction != nil { 363 if t.Log != nil { 364 _, tokenAddress, txFrom, txTo = w_common.ExtractTokenTransferData(t.Type, t.Log, t.Transaction) 365 tokenID = t.TokenID 366 // Zero tokenID can be used for ERC721 and ERC1155 transfers but when serialzed/deserialized it becomes nil 367 // as 0 value of big.Int bytes is nil. 368 if tokenID == nil && (t.Type == w_common.Erc721Transfer || t.Type == w_common.Erc1155Transfer) { 369 tokenID = big.NewInt(0) 370 } 371 txValue = t.TokenValue 372 } else { 373 txValue = new(big.Int).Set(t.Transaction.Value()) 374 txFrom = &t.From 375 txTo = t.Transaction.To() 376 } 377 378 txType = new(uint8) 379 *txType = t.Transaction.Type() 380 txProtected = new(bool) 381 *txProtected = t.Transaction.Protected() 382 txGas = new(uint64) 383 *txGas = t.Transaction.Gas() 384 txGasPrice = t.Transaction.GasPrice() 385 txGasTipCap = t.Transaction.GasTipCap() 386 txGasFeeCap = t.Transaction.GasFeeCap() 387 txNonce = new(uint64) 388 *txNonce = t.Transaction.Nonce() 389 txSize = new(uint64) 390 *txSize = t.Transaction.Size() 391 } 392 393 dbFields := transferDBFields{ 394 chainID: chainID, 395 id: t.ID, 396 blockHash: t.BlockHash, 397 blockNumber: t.BlockNumber, 398 timestamp: t.Timestamp, 399 address: t.Address, 400 transaction: t.Transaction, 401 sender: t.From, 402 receipt: t.Receipt, 403 log: t.Log, 404 transferType: t.Type, 405 baseGasFees: t.BaseGasFees, 406 multiTransactionID: t.MultiTransactionID, 407 receiptStatus: receiptStatus, 408 receiptType: receiptType, 409 txHash: txHash, 410 logIndex: logIndex, 411 receiptBlockHash: blockHash, 412 cumulativeGasUsed: cumulativeGasUsed, 413 contractAddress: contractAddress, 414 gasUsed: gasUsed, 415 transactionIndex: transactionIndex, 416 txType: txType, 417 txProtected: txProtected, 418 txGas: txGas, 419 txGasPrice: txGasPrice, 420 txGasTipCap: txGasTipCap, 421 txGasFeeCap: txGasFeeCap, 422 txValue: txValue, 423 txNonce: txNonce, 424 txSize: txSize, 425 tokenAddress: tokenAddress, 426 tokenID: tokenID, 427 txFrom: txFrom, 428 txTo: txTo, 429 } 430 txsDBFields = append(txsDBFields, dbFields) 431 } 432 433 return updateOrInsertTransfersDBFields(creator, txsDBFields) 434 } 435 436 type transferDBFields struct { 437 chainID uint64 438 id common.Hash 439 blockHash common.Hash 440 blockNumber *big.Int 441 timestamp uint64 442 address common.Address 443 transaction *types.Transaction 444 sender common.Address 445 receipt *types.Receipt 446 log *types.Log 447 transferType w_common.Type 448 baseGasFees string 449 multiTransactionID w_common.MultiTransactionIDType 450 receiptStatus *uint64 451 receiptType *uint8 452 txHash *common.Hash 453 logIndex *uint 454 receiptBlockHash *common.Hash 455 cumulativeGasUsed *uint64 456 contractAddress *common.Address 457 gasUsed *uint64 458 transactionIndex *uint 459 txType *uint8 460 txProtected *bool 461 txGas *uint64 462 txGasPrice *big.Int 463 txGasTipCap *big.Int 464 txGasFeeCap *big.Int 465 txValue *big.Int 466 txNonce *uint64 467 txSize *uint64 468 tokenAddress *common.Address 469 tokenID *big.Int 470 txFrom *common.Address 471 txTo *common.Address 472 } 473 474 func updateOrInsertTransfersDBFields(creator statementCreator, transfers []transferDBFields) error { 475 insert, err := creator.Prepare(`INSERT OR REPLACE INTO transfers 476 (network_id, hash, blk_hash, blk_number, timestamp, address, tx, sender, receipt, log, type, loaded, base_gas_fee, multi_transaction_id, 477 status, receipt_type, tx_hash, log_index, block_hash, cumulative_gas_used, contract_address, gas_used, tx_index, 478 tx_type, protected, gas_limit, gas_price_clamped64, gas_tip_cap_clamped64, gas_fee_cap_clamped64, amount_padded128hex, account_nonce, size, token_address, token_id, tx_from_address, tx_to_address) 479 VALUES 480 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) 481 if err != nil { 482 return err 483 } 484 for _, t := range transfers { 485 txGasPrice := sqlite.BigIntToClampedInt64(t.txGasPrice) 486 txGasTipCap := sqlite.BigIntToClampedInt64(t.txGasTipCap) 487 txGasFeeCap := sqlite.BigIntToClampedInt64(t.txGasFeeCap) 488 txValue := sqlite.BigIntToPadded128BitsStr(t.txValue) 489 490 _, err = insert.Exec(t.chainID, t.id, t.blockHash, (*bigint.SQLBigInt)(t.blockNumber), t.timestamp, t.address, &JSONBlob{t.transaction}, t.sender, &JSONBlob{t.receipt}, &JSONBlob{t.log}, t.transferType, t.baseGasFees, t.multiTransactionID, 491 t.receiptStatus, t.receiptType, t.txHash, t.logIndex, t.receiptBlockHash, t.cumulativeGasUsed, t.contractAddress, t.gasUsed, t.transactionIndex, 492 t.txType, t.txProtected, t.txGas, txGasPrice, txGasTipCap, txGasFeeCap, txValue, t.txNonce, t.txSize, t.tokenAddress, (*bigint.SQLBigIntBytes)(t.tokenID), t.txFrom, t.txTo) 493 if err != nil { 494 log.Error("can't save transfer", "b-hash", t.blockHash, "b-n", t.blockNumber, "a", t.address, "h", t.id) 495 return err 496 } 497 } 498 499 for _, t := range transfers { 500 err = removeGasOnlyEthTransfer(creator, t) 501 if err != nil { 502 log.Error("can't remove gas only eth transfer", "b-hash", t.blockHash, "b-n", t.blockNumber, "a", t.address, "h", t.id, "err", err) 503 // no return err, since it's not critical 504 } 505 } 506 return nil 507 } 508 509 func removeGasOnlyEthTransfer(creator statementCreator, t transferDBFields) error { 510 if t.transferType == w_common.EthTransfer { 511 countQuery, err := creator.Prepare(`SELECT COUNT(*) FROM transfers WHERE tx_hash = ?`) 512 if err != nil { 513 return err 514 } 515 defer countQuery.Close() 516 517 var count int 518 err = countQuery.QueryRow(t.txHash).Scan(&count) 519 if err != nil { 520 return err 521 } 522 523 // If there's only one (or none), return without deleting 524 if count <= 1 { 525 log.Debug("Only one or no transfer found with the same tx_hash, skipping deletion.") 526 return nil 527 } 528 } 529 query, err := creator.Prepare(`DELETE FROM transfers WHERE tx_hash = ? AND address = ? AND network_id = ? AND account_nonce = ? AND type = 'eth' AND amount_padded128hex = '00000000000000000000000000000000'`) 530 if err != nil { 531 return err 532 } 533 defer query.Close() 534 535 res, err := query.Exec(t.txHash, t.address, t.chainID, t.txNonce) 536 if err != nil { 537 return err 538 } 539 count, err := res.RowsAffected() 540 if err != nil { 541 return err 542 } 543 log.Debug("removeGasOnlyEthTransfer rows deleted", "count", count) 544 return nil 545 } 546 547 // markBlocksAsLoaded(chainID, tx, address, blockNumbers) 548 // In case block contains both ETH and token transfers, it will be marked as loaded on ETH transfer processing. 549 // This is not a problem since for token transfers we have preloaded transactions and blocks 'loaded' flag is needed 550 // for ETH transfers only. 551 func markBlocksAsLoaded(chainID uint64, creator statementCreator, address common.Address, blocks []*big.Int) error { 552 update, err := creator.Prepare("UPDATE blocks SET loaded=? WHERE address=? AND blk_number=? AND network_id=?") 553 if err != nil { 554 return err 555 } 556 557 for _, block := range blocks { 558 _, err := update.Exec(true, address, (*bigint.SQLBigInt)(block), chainID) 559 if err != nil { 560 return err 561 } 562 } 563 return nil 564 } 565 566 // GetOwnedMultiTransactionID returns sql.ErrNoRows if no transaction is found for the given identity 567 func GetOwnedMultiTransactionID(tx *sql.Tx, chainID w_common.ChainID, hash common.Hash, address common.Address) (mTID int64, err error) { 568 row := tx.QueryRow(`SELECT COALESCE(multi_transaction_id, 0) FROM transfers WHERE network_id = ? AND tx_hash = ? AND address = ?`, chainID, hash, address) 569 err = row.Scan(&mTID) 570 if err != nil { 571 return 0, err 572 } 573 return mTID, nil 574 } 575 576 func (db *Database) GetLatestCollectibleTransfer(address common.Address, id thirdparty.CollectibleUniqueID) (*Transfer, error) { 577 query := newTransfersQuery(). 578 FilterAddress(address). 579 FilterNetwork(uint64(id.ContractID.ChainID)). 580 FilterTokenAddress(id.ContractID.Address). 581 FilterTokenID(id.TokenID.Int). 582 FilterLoaded(1). 583 SortByTimestamp(false). 584 Limit(1) 585 rows, err := db.client.Query(query.String(), query.Args()...) 586 if err != nil { 587 return nil, err 588 } 589 defer rows.Close() 590 591 transfers, err := query.TransferScan(rows) 592 if err == sql.ErrNoRows || len(transfers) == 0 { 593 return nil, nil 594 } else if err != nil { 595 return nil, err 596 } 597 598 return &transfers[0], nil 599 } 600 601 // Delete blocks for address and chainID 602 // Transfers will be deleted by cascade 603 func deleteBlocks(creator statementCreator, address common.Address) error { 604 delete, err := creator.Prepare("DELETE FROM blocks WHERE address = ?") 605 if err != nil { 606 return err 607 } 608 609 _, err = delete.Exec(address) 610 return err 611 } 612 613 func getAddresses(creator statementCreator) (rst []common.Address, err error) { 614 stmt, err := creator.Prepare(`SELECT address FROM transfers UNION SELECT address FROM blocks UNION 615 SELECT address FROM blocks_ranges_sequential UNION SELECT address FROM blocks_ranges`) 616 if err != nil { 617 return 618 } 619 rows, err := stmt.Query() 620 if err != nil { 621 return nil, err 622 } 623 defer rows.Close() 624 625 address := common.Address{} 626 for rows.Next() { 627 err = rows.Scan(&address) 628 if err != nil { 629 return nil, err 630 } 631 rst = append(rst, address) 632 } 633 634 return rst, nil 635 }