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, &timestamp, &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  }