github.com/status-im/status-go@v1.1.0/transactions/pendingtxtracker.go (about) 1 package transactions 2 3 import ( 4 "context" 5 "database/sql" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "math/big" 10 "strings" 11 "time" 12 13 eth "github.com/ethereum/go-ethereum/common" 14 "github.com/ethereum/go-ethereum/core/types" 15 "github.com/ethereum/go-ethereum/event" 16 "github.com/ethereum/go-ethereum/log" 17 "github.com/ethereum/go-ethereum/p2p" 18 ethrpc "github.com/ethereum/go-ethereum/rpc" 19 20 "github.com/status-im/status-go/rpc" 21 "github.com/status-im/status-go/services/rpcfilters" 22 "github.com/status-im/status-go/services/wallet/bigint" 23 "github.com/status-im/status-go/services/wallet/common" 24 wallet_common "github.com/status-im/status-go/services/wallet/common" 25 "github.com/status-im/status-go/services/wallet/walletevent" 26 ) 27 28 const ( 29 // EventPendingTransactionUpdate is emitted when a pending transaction is updated (added or deleted). Carries PendingTxUpdatePayload in message 30 EventPendingTransactionUpdate walletevent.EventType = "pending-transaction-update" 31 // EventPendingTransactionStatusChanged carries StatusChangedPayload in message 32 EventPendingTransactionStatusChanged walletevent.EventType = "pending-transaction-status-changed" 33 34 PendingCheckInterval = 10 * time.Second 35 36 GetTransactionReceiptRPCName = "eth_getTransactionReceipt" 37 ) 38 39 var ( 40 ErrStillPending = errors.New("transaction is still pending") 41 ) 42 43 type TxStatus = string 44 45 // Values for status column in pending_transactions 46 const ( 47 Pending TxStatus = "Pending" 48 Success TxStatus = "Success" 49 Failed TxStatus = "Failed" 50 ) 51 52 type AutoDeleteType = bool 53 54 const ( 55 AutoDelete AutoDeleteType = true 56 Keep AutoDeleteType = false 57 ) 58 59 type TxIdentity struct { 60 ChainID common.ChainID `json:"chainId"` 61 Hash eth.Hash `json:"hash"` 62 } 63 64 type PendingTxUpdatePayload struct { 65 TxIdentity 66 Deleted bool `json:"deleted"` 67 } 68 69 type StatusChangedPayload struct { 70 TxIdentity 71 Status TxStatus `json:"status"` 72 } 73 74 // PendingTxTracker implements StatusService in common/status_node_service.go 75 type PendingTxTracker struct { 76 db *sql.DB 77 rpcClient rpc.ClientInterface 78 79 rpcFilter *rpcfilters.Service 80 eventFeed *event.Feed 81 82 taskRunner *ConditionalRepeater 83 log log.Logger 84 } 85 86 func NewPendingTxTracker(db *sql.DB, rpcClient rpc.ClientInterface, rpcFilter *rpcfilters.Service, eventFeed *event.Feed, checkInterval time.Duration) *PendingTxTracker { 87 tm := &PendingTxTracker{ 88 db: db, 89 rpcClient: rpcClient, 90 eventFeed: eventFeed, 91 rpcFilter: rpcFilter, 92 log: log.New("package", "status-go/transactions.PendingTxTracker"), 93 } 94 tm.taskRunner = NewConditionalRepeater(checkInterval, func(ctx context.Context) bool { 95 return tm.fetchAndUpdateDB(ctx) 96 }) 97 return tm 98 } 99 100 type txStatusRes struct { 101 Status TxStatus 102 hash eth.Hash 103 } 104 105 func (tm *PendingTxTracker) fetchAndUpdateDB(ctx context.Context) bool { 106 res := WorkNotDone 107 108 txs, err := tm.GetAllPending() 109 if err != nil { 110 tm.log.Error("Failed to get pending transactions", "error", err) 111 return WorkDone 112 } 113 tm.log.Debug("Checking for PT status", "count", len(txs)) 114 115 txsMap := make(map[common.ChainID][]eth.Hash) 116 for _, tx := range txs { 117 chainID := tx.ChainID 118 txsMap[chainID] = append(txsMap[chainID], tx.Hash) 119 } 120 121 doneCount := 0 122 // Batch request for each chain 123 for chainID, txs := range txsMap { 124 tm.log.Debug("Processing PTs", "chainID", chainID, "count", len(txs)) 125 batchRes, err := fetchBatchTxStatus(ctx, tm.rpcClient, chainID, txs, tm.log) 126 if err != nil { 127 tm.log.Error("Failed to batch fetch pending transactions status for", "chainID", chainID, "error", err) 128 continue 129 } 130 if len(batchRes) == 0 { 131 tm.log.Debug("No change to PTs status", "chainID", chainID) 132 continue 133 } 134 tm.log.Debug("PTs done", "chainID", chainID, "count", len(batchRes)) 135 doneCount += len(batchRes) 136 137 updateRes, err := tm.updateDBStatus(ctx, chainID, batchRes) 138 if err != nil { 139 tm.log.Error("Failed to update pending transactions status for", "chainID", chainID, "error", err) 140 continue 141 } 142 143 tm.log.Debug("Emit notifications for PTs", "chainID", chainID, "count", len(updateRes)) 144 tm.emitNotifications(chainID, updateRes) 145 } 146 147 if len(txs) == doneCount { 148 res = WorkDone 149 } 150 151 tm.log.Debug("Done PTs iteration", "count", doneCount, "completed", res) 152 153 return res 154 } 155 156 type nullableReceipt struct { 157 *types.Receipt 158 } 159 160 func (nr *nullableReceipt) UnmarshalJSON(data []byte) error { 161 transactionNotAvailable := (string(data) == "null") 162 if transactionNotAvailable { 163 return nil 164 } 165 return json.Unmarshal(data, &nr.Receipt) 166 } 167 168 // fetchBatchTxStatus returns not pending transactions (confirmed or errored) 169 // it excludes the still pending or errored request from the result 170 func fetchBatchTxStatus(ctx context.Context, rpcClient rpc.ClientInterface, chainID common.ChainID, hashes []eth.Hash, log log.Logger) ([]txStatusRes, error) { 171 chainClient, err := rpcClient.AbstractEthClient(chainID) 172 if err != nil { 173 log.Error("Failed to get chain client", "error", err) 174 return nil, err 175 } 176 177 reqCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 178 defer cancel() 179 180 batch := make([]ethrpc.BatchElem, 0, len(hashes)) 181 for _, hash := range hashes { 182 batch = append(batch, ethrpc.BatchElem{ 183 Method: GetTransactionReceiptRPCName, 184 Args: []interface{}{hash}, 185 Result: new(nullableReceipt), 186 }) 187 } 188 189 err = chainClient.BatchCallContext(reqCtx, batch) 190 if err != nil { 191 log.Error("Transactions request fail", "error", err) 192 return nil, err 193 } 194 195 res := make([]txStatusRes, 0, len(batch)) 196 for i, b := range batch { 197 err := b.Error 198 if err != nil { 199 log.Error("Failed to get transaction", "error", err, "hash", hashes[i]) 200 continue 201 } 202 203 if b.Result == nil { 204 log.Error("Transaction not found", "hash", hashes[i]) 205 continue 206 } 207 208 receiptWrapper, ok := b.Result.(*nullableReceipt) 209 if !ok { 210 log.Error("Failed to cast transaction receipt", "hash", hashes[i]) 211 continue 212 } 213 214 if receiptWrapper == nil || receiptWrapper.Receipt == nil { 215 // the transaction is not available yet 216 continue 217 } 218 219 receipt := receiptWrapper.Receipt 220 isPending := receipt != nil && receipt.BlockNumber == nil 221 if !isPending { 222 var status TxStatus 223 if receipt.Status == types.ReceiptStatusSuccessful { 224 status = Success 225 } else { 226 status = Failed 227 } 228 res = append(res, txStatusRes{ 229 hash: hashes[i], 230 Status: status, 231 }) 232 } 233 } 234 return res, nil 235 } 236 237 // updateDBStatus returns entries that were updated only 238 func (tm *PendingTxTracker) updateDBStatus(ctx context.Context, chainID common.ChainID, statuses []txStatusRes) ([]txStatusRes, error) { 239 res := make([]txStatusRes, 0, len(statuses)) 240 tx, err := tm.db.BeginTx(ctx, nil) 241 if err != nil { 242 return nil, fmt.Errorf("failed to begin transaction: %w", err) 243 } 244 245 updateStmt, err := tx.PrepareContext(ctx, `UPDATE pending_transactions SET status = ? WHERE network_id = ? AND hash = ?`) 246 if err != nil { 247 rollErr := tx.Rollback() 248 if rollErr != nil { 249 err = fmt.Errorf("failed to rollback transaction due to: %w", err) 250 } 251 return nil, fmt.Errorf("failed to prepare update statement: %w", err) 252 } 253 254 checkAutoDelStmt, err := tx.PrepareContext(ctx, `SELECT auto_delete FROM pending_transactions WHERE network_id = ? AND hash = ?`) 255 if err != nil { 256 rollErr := tx.Rollback() 257 if rollErr != nil { 258 err = fmt.Errorf("failed to rollback transaction: %w", err) 259 } 260 return nil, fmt.Errorf("failed to prepare auto delete statement: %w", err) 261 } 262 263 notifyFunctions := make([]func(), 0, len(statuses)) 264 for _, br := range statuses { 265 row := checkAutoDelStmt.QueryRowContext(ctx, chainID, br.hash) 266 var autoDel bool 267 err = row.Scan(&autoDel) 268 if err != nil { 269 if err == sql.ErrNoRows { 270 tm.log.Warn("Missing entry while checking for auto_delete", "hash", br.hash) 271 } else { 272 tm.log.Error("Failed to retrieve auto_delete for pending transaction", "error", err, "hash", br.hash) 273 } 274 continue 275 } 276 277 if autoDel { 278 notifyFn, err := tm.DeleteBySQLTx(tx, chainID, br.hash) 279 if err != nil && err != ErrStillPending { 280 tm.log.Error("Failed to delete pending transaction", "error", err, "hash", br.hash) 281 continue 282 } 283 notifyFunctions = append(notifyFunctions, notifyFn) 284 } else { 285 // If the entry was not deleted, update the status 286 txStatus := br.Status 287 288 res, err := updateStmt.ExecContext(ctx, txStatus, chainID, br.hash) 289 if err != nil { 290 tm.log.Error("Failed to update pending transaction status", "error", err, "hash", br.hash) 291 continue 292 } 293 affected, err := res.RowsAffected() 294 if err != nil { 295 tm.log.Error("Failed to get updated rows", "error", err, "hash", br.hash) 296 continue 297 } 298 299 if affected == 0 { 300 tm.log.Warn("Missing entry to update for", "hash", br.hash) 301 continue 302 } 303 } 304 305 res = append(res, br) 306 } 307 308 err = tx.Commit() 309 if err != nil { 310 return nil, fmt.Errorf("failed to commit transaction: %w", err) 311 } 312 313 for _, fn := range notifyFunctions { 314 fn() 315 } 316 317 return res, nil 318 } 319 320 func (tm *PendingTxTracker) emitNotifications(chainID common.ChainID, changes []txStatusRes) { 321 if tm.eventFeed != nil { 322 for _, change := range changes { 323 payload := StatusChangedPayload{ 324 TxIdentity: TxIdentity{ 325 ChainID: chainID, 326 Hash: change.hash, 327 }, 328 Status: change.Status, 329 } 330 331 jsonPayload, err := json.Marshal(payload) 332 if err != nil { 333 tm.log.Error("Failed to marshal pending transaction status", "error", err, "hash", change.hash) 334 continue 335 } 336 tm.eventFeed.Send(walletevent.Event{ 337 Type: EventPendingTransactionStatusChanged, 338 ChainID: uint64(chainID), 339 Message: string(jsonPayload), 340 }) 341 } 342 } 343 } 344 345 // PendingTransaction called with autoDelete = false will keep the transaction in the database until it is confirmed by the caller using Delete 346 func (tm *PendingTxTracker) TrackPendingTransaction(chainID common.ChainID, hash eth.Hash, from eth.Address, to eth.Address, trType PendingTrxType, autoDelete AutoDeleteType, additionalData string) error { 347 err := tm.addPending(&PendingTransaction{ 348 ChainID: chainID, 349 Hash: hash, 350 From: from, 351 To: to, 352 Timestamp: uint64(time.Now().Unix()), 353 Type: trType, 354 AutoDelete: &autoDelete, 355 AdditionalData: additionalData, 356 }) 357 if err != nil { 358 return err 359 } 360 361 tm.taskRunner.RunUntilDone() 362 363 return nil 364 } 365 366 func (tm *PendingTxTracker) Start() error { 367 tm.taskRunner.RunUntilDone() 368 return nil 369 } 370 371 // APIs returns a list of new APIs. 372 func (tm *PendingTxTracker) APIs() []ethrpc.API { 373 return []ethrpc.API{ 374 { 375 Namespace: "pending", 376 Version: "0.1.0", 377 Service: tm, 378 Public: true, 379 }, 380 } 381 } 382 383 // Protocols returns a new protocols list. In this case, there are none. 384 func (tm *PendingTxTracker) Protocols() []p2p.Protocol { 385 return []p2p.Protocol{} 386 } 387 388 func (tm *PendingTxTracker) Stop() error { 389 tm.taskRunner.Stop() 390 return nil 391 } 392 393 type PendingTrxType string 394 395 const ( 396 RegisterENS PendingTrxType = "RegisterENS" 397 ReleaseENS PendingTrxType = "ReleaseENS" 398 SetPubKey PendingTrxType = "SetPubKey" 399 BuyStickerPack PendingTrxType = "BuyStickerPack" 400 WalletTransfer PendingTrxType = "WalletTransfer" 401 DeployCommunityToken PendingTrxType = "DeployCommunityToken" 402 AirdropCommunityToken PendingTrxType = "AirdropCommunityToken" 403 RemoteDestructCollectible PendingTrxType = "RemoteDestructCollectible" 404 BurnCommunityToken PendingTrxType = "BurnCommunityToken" 405 DeployOwnerToken PendingTrxType = "DeployOwnerToken" 406 SetSignerPublicKey PendingTrxType = "SetSignerPublicKey" 407 WalletConnectTransfer PendingTrxType = "WalletConnectTransfer" 408 ) 409 410 type PendingTransaction struct { 411 Hash eth.Hash `json:"hash"` 412 Timestamp uint64 `json:"timestamp"` 413 Value bigint.BigInt `json:"value"` 414 From eth.Address `json:"from"` 415 To eth.Address `json:"to"` 416 Data string `json:"data"` 417 Symbol string `json:"symbol"` 418 GasPrice bigint.BigInt `json:"gasPrice"` 419 GasLimit bigint.BigInt `json:"gasLimit"` 420 Type PendingTrxType `json:"type"` 421 AdditionalData string `json:"additionalData"` 422 ChainID common.ChainID `json:"network_id"` 423 MultiTransactionID wallet_common.MultiTransactionIDType `json:"multi_transaction_id"` 424 Nonce uint64 `json:"nonce"` 425 426 // nil will insert the default value (Pending) in DB 427 Status *TxStatus `json:"status,omitempty"` 428 // nil will insert the default value (true) in DB 429 AutoDelete *bool `json:"autoDelete,omitempty"` 430 } 431 432 const selectFromPending = `SELECT hash, timestamp, value, from_address, to_address, data, 433 symbol, gas_price, gas_limit, type, additional_data, 434 network_id, COALESCE(multi_transaction_id, 0), status, auto_delete, nonce 435 FROM pending_transactions 436 ` 437 438 func rowsToTransactions(rows *sql.Rows) (transactions []*PendingTransaction, err error) { 439 for rows.Next() { 440 transaction := &PendingTransaction{ 441 Value: bigint.BigInt{Int: new(big.Int)}, 442 GasPrice: bigint.BigInt{Int: new(big.Int)}, 443 GasLimit: bigint.BigInt{Int: new(big.Int)}, 444 } 445 446 transaction.Status = new(TxStatus) 447 transaction.AutoDelete = new(bool) 448 err := rows.Scan(&transaction.Hash, 449 &transaction.Timestamp, 450 (*bigint.SQLBigIntBytes)(transaction.Value.Int), 451 &transaction.From, 452 &transaction.To, 453 &transaction.Data, 454 &transaction.Symbol, 455 (*bigint.SQLBigIntBytes)(transaction.GasPrice.Int), 456 (*bigint.SQLBigIntBytes)(transaction.GasLimit.Int), 457 &transaction.Type, 458 &transaction.AdditionalData, 459 &transaction.ChainID, 460 &transaction.MultiTransactionID, 461 transaction.Status, 462 transaction.AutoDelete, 463 &transaction.Nonce, 464 ) 465 if err != nil { 466 return nil, err 467 } 468 469 transactions = append(transactions, transaction) 470 } 471 return transactions, nil 472 } 473 474 func (tm *PendingTxTracker) GetAllPending() ([]*PendingTransaction, error) { 475 if tm.db == nil { 476 return nil, errors.New("database is not initialized") 477 } 478 rows, err := tm.db.Query(selectFromPending+"WHERE status = ?", Pending) 479 if err != nil { 480 return nil, err 481 } 482 defer rows.Close() 483 484 return rowsToTransactions(rows) 485 } 486 487 func (tm *PendingTxTracker) GetPendingByAddress(chainIDs []uint64, address eth.Address) ([]*PendingTransaction, error) { 488 if len(chainIDs) == 0 { 489 return nil, errors.New("GetPendingByAddress: at least 1 chainID is required") 490 } 491 492 inVector := strings.Repeat("?, ", len(chainIDs)-1) + "?" 493 var parameters []interface{} 494 for _, c := range chainIDs { 495 parameters = append(parameters, c) 496 } 497 498 parameters = append(parameters, address) 499 500 rows, err := tm.db.Query(fmt.Sprintf(selectFromPending+"WHERE network_id in (%s) AND from_address = ?", inVector), parameters...) 501 if err != nil { 502 return nil, err 503 } 504 defer rows.Close() 505 506 return rowsToTransactions(rows) 507 } 508 509 // GetPendingEntry returns sql.ErrNoRows if no pending transaction is found for the given identity 510 func (tm *PendingTxTracker) GetPendingEntry(chainID common.ChainID, hash eth.Hash) (*PendingTransaction, error) { 511 rows, err := tm.db.Query(selectFromPending+"WHERE network_id = ? AND hash = ?", chainID, hash) 512 if err != nil { 513 return nil, err 514 } 515 defer rows.Close() 516 517 trs, err := rowsToTransactions(rows) 518 if err != nil { 519 return nil, err 520 } 521 522 if len(trs) == 0 { 523 return nil, sql.ErrNoRows 524 } 525 return trs[0], nil 526 } 527 528 func (tm *PendingTxTracker) CountPendingTxsFromNonce(chainID common.ChainID, address eth.Address, nonce uint64) (pendingTx uint64, err error) { 529 err = tm.db.QueryRow(` 530 SELECT 531 COUNT(nonce) 532 FROM 533 pending_transactions 534 WHERE 535 network_id = ? 536 AND 537 from_address = ? 538 AND 539 nonce >= ?`, 540 chainID, address, nonce). 541 Scan(&pendingTx) 542 return 543 } 544 545 // StoreAndTrackPendingTx store the details of a pending transaction and track it until it is mined 546 func (tm *PendingTxTracker) StoreAndTrackPendingTx(transaction *PendingTransaction) error { 547 err := tm.addPending(transaction) 548 if err != nil { 549 return err 550 } 551 552 tm.taskRunner.RunUntilDone() 553 554 return err 555 } 556 557 func (tm *PendingTxTracker) addPending(transaction *PendingTransaction) error { 558 var notifyFn func() 559 tx, err := tm.db.Begin() 560 if err != nil { 561 return err 562 } 563 defer func() { 564 if err == nil { 565 err = tx.Commit() 566 if notifyFn != nil { 567 notifyFn() 568 } 569 return 570 } 571 _ = tx.Rollback() 572 }() 573 574 exists := true 575 var hash eth.Hash 576 577 err = tx.QueryRow(` 578 SELECT hash 579 FROM 580 pending_transactions 581 WHERE 582 network_id = ? 583 AND 584 from_address = ? 585 AND 586 nonce = ? 587 `, 588 transaction.ChainID, 589 transaction.From, 590 transaction.Nonce). 591 Scan(&hash) 592 if err != nil { 593 if err == sql.ErrNoRows { 594 exists = false 595 } else { 596 return err 597 } 598 } 599 600 if exists { 601 notifyFn, err = tm.DeleteBySQLTx(tx, transaction.ChainID, hash) 602 if err != nil && err != ErrStillPending { 603 return err 604 } 605 } 606 607 // TODO: maybe we should think of making (network_id, from_address, nonce) as primary key instead (network_id, hash) ???? 608 var insert *sql.Stmt 609 insert, err = tx.Prepare(`INSERT OR REPLACE INTO pending_transactions 610 (network_id, hash, timestamp, value, from_address, to_address, 611 data, symbol, gas_price, gas_limit, type, additional_data, multi_transaction_id, status, 612 auto_delete, nonce) 613 VALUES 614 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? , ?, ?)`) 615 if err != nil { 616 return err 617 } 618 defer insert.Close() 619 620 _, err = insert.Exec( 621 transaction.ChainID, 622 transaction.Hash, 623 transaction.Timestamp, 624 (*bigint.SQLBigIntBytes)(transaction.Value.Int), 625 transaction.From, 626 transaction.To, 627 transaction.Data, 628 transaction.Symbol, 629 (*bigint.SQLBigIntBytes)(transaction.GasPrice.Int), 630 (*bigint.SQLBigIntBytes)(transaction.GasLimit.Int), 631 transaction.Type, 632 transaction.AdditionalData, 633 transaction.MultiTransactionID, 634 transaction.Status, 635 transaction.AutoDelete, 636 transaction.Nonce, 637 ) 638 // Notify listeners of new pending transaction (used in activity history) 639 if err == nil { 640 tm.notifyPendingTransactionListeners(PendingTxUpdatePayload{ 641 TxIdentity: TxIdentity{ 642 ChainID: transaction.ChainID, 643 Hash: transaction.Hash, 644 }, 645 Deleted: false, 646 }, []eth.Address{transaction.From, transaction.To}, transaction.Timestamp) 647 } 648 if tm.rpcFilter != nil { 649 tm.rpcFilter.TriggerTransactionSentToUpstreamEvent(&rpcfilters.PendingTxInfo{ 650 Hash: transaction.Hash, 651 Type: string(transaction.Type), 652 From: transaction.From, 653 ChainID: uint64(transaction.ChainID), 654 }) 655 } 656 return err 657 } 658 659 func (tm *PendingTxTracker) notifyPendingTransactionListeners(payload PendingTxUpdatePayload, addresses []eth.Address, timestamp uint64) { 660 jsonPayload, err := json.Marshal(payload) 661 if err != nil { 662 tm.log.Error("Failed to marshal PendingTxUpdatePayload", "error", err, "hash", payload.Hash) 663 return 664 } 665 666 if tm.eventFeed != nil { 667 tm.eventFeed.Send(walletevent.Event{ 668 Type: EventPendingTransactionUpdate, 669 ChainID: uint64(payload.ChainID), 670 Accounts: addresses, 671 At: int64(timestamp), 672 Message: string(jsonPayload), 673 }) 674 } 675 } 676 677 // DeleteBySQLTx returns ErrStillPending if the transaction is still pending 678 func (tm *PendingTxTracker) DeleteBySQLTx(tx *sql.Tx, chainID common.ChainID, hash eth.Hash) (notify func(), err error) { 679 row := tx.QueryRow(`SELECT from_address, to_address, timestamp, status FROM pending_transactions WHERE network_id = ? AND hash = ?`, chainID, hash) 680 var from, to eth.Address 681 var timestamp uint64 682 var status TxStatus 683 err = row.Scan(&from, &to, ×tamp, &status) 684 if err != nil { 685 return nil, err 686 } 687 688 _, err = tx.Exec(`DELETE FROM pending_transactions WHERE network_id = ? AND hash = ?`, chainID, hash) 689 if err != nil { 690 return nil, err 691 } 692 693 if err == nil && status == Pending { 694 err = ErrStillPending 695 } 696 return func() { 697 tm.notifyPendingTransactionListeners(PendingTxUpdatePayload{ 698 TxIdentity: TxIdentity{ 699 ChainID: chainID, 700 Hash: hash, 701 }, 702 Deleted: true, 703 }, []eth.Address{from, to}, timestamp) 704 }, err 705 } 706 707 // GetOwnedPendingStatus returns sql.ErrNoRows if no pending transaction is found for the given identity 708 func GetOwnedPendingStatus(tx *sql.Tx, chainID common.ChainID, hash eth.Hash, ownerAddress eth.Address) (txType *PendingTrxType, mTID *int64, err error) { 709 row := tx.QueryRow(`SELECT type, multi_transaction_id FROM pending_transactions WHERE network_id = ? AND hash = ? AND from_address = ?`, chainID, hash, ownerAddress) 710 txType = new(PendingTrxType) 711 mTID = new(int64) 712 err = row.Scan(txType, mTID) 713 if err != nil { 714 return nil, nil, err 715 } 716 return txType, mTID, nil 717 } 718 719 // Watch returns sql.ErrNoRows if no pending transaction is found for the given identity 720 // tx.Status is not nill if err is nil 721 func (tm *PendingTxTracker) Watch(ctx context.Context, chainID common.ChainID, hash eth.Hash) (*TxStatus, error) { 722 tx, err := tm.GetPendingEntry(chainID, hash) 723 if err != nil { 724 return nil, err 725 } 726 727 return tx.Status, nil 728 } 729 730 // Delete returns ErrStillPending if the deleted transaction was still pending 731 // The transactions are suppose to be deleted by the client only after they are confirmed 732 func (tm *PendingTxTracker) Delete(ctx context.Context, chainID common.ChainID, transactionHash eth.Hash) error { 733 tx, err := tm.db.BeginTx(ctx, nil) 734 if err != nil { 735 return fmt.Errorf("failed to begin transaction: %w", err) 736 } 737 738 notifyFn, err := tm.DeleteBySQLTx(tx, chainID, transactionHash) 739 if err != nil && err != ErrStillPending { 740 rollErr := tx.Rollback() 741 if rollErr != nil { 742 return fmt.Errorf("failed to rollback transaction due to error: %w", err) 743 } 744 return err 745 } 746 747 commitErr := tx.Commit() 748 if commitErr != nil { 749 return fmt.Errorf("failed to commit transaction: %w", commitErr) 750 } 751 notifyFn() 752 return err 753 }