code.vegaprotocol.io/vega@v0.79.0/blockexplorer/store/transactions.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package store
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"sort"
    23  	"strings"
    24  
    25  	"code.vegaprotocol.io/vega/blockexplorer/entities"
    26  	pb "code.vegaprotocol.io/vega/protos/blockexplorer/api/v1"
    27  
    28  	"github.com/georgysavva/scany/pgxscan"
    29  )
    30  
    31  var (
    32  	ErrTxNotFound      = errors.New("transaction not found")
    33  	ErrMultipleTxFound = errors.New("multiple transactions found")
    34  )
    35  
    36  func (s *Store) GetTransaction(ctx context.Context, txID string) (*pb.Transaction, error) {
    37  	txID = strings.ToUpper(txID)
    38  
    39  	query := `SELECT t.rowid, t.block_height, t.index, t.created_at, t.tx_hash, t.tx_result, t.cmd_type, t.submitter FROM tx_results t WHERE t.tx_hash=$1`
    40  	var rows []entities.TxResultRow
    41  
    42  	if err := pgxscan.Select(ctx, s.pool, &rows, query, txID); err != nil {
    43  		return nil, fmt.Errorf("querying tx_results: %w", err)
    44  	}
    45  
    46  	if len(rows) == 0 {
    47  		return nil, ErrTxNotFound
    48  	}
    49  
    50  	if len(rows) > 1 {
    51  		return nil, ErrMultipleTxFound
    52  	}
    53  
    54  	tx, err := rows[0].ToProto()
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  	return tx, nil
    59  }
    60  
    61  func (s *Store) ListTransactions(ctx context.Context,
    62  	filters map[string]string,
    63  	cmdTypes, exclCmdTypes, parties []string,
    64  	first uint32,
    65  	after *entities.TxCursor,
    66  	last uint32,
    67  	before *entities.TxCursor,
    68  ) ([]*pb.Transaction, error) {
    69  	query := `SELECT t.rowid, t.block_height, t.index, t.created_at, t.tx_hash, t.tx_result, t.cmd_type, t.submitter FROM tx_results t`
    70  
    71  	args := []interface{}{}
    72  	predicates := []string{}
    73  
    74  	limit := uint32(0)
    75  
    76  	sortOrder := "desc"
    77  
    78  	if first > 0 {
    79  		// We want the N most recent transactions, descending on block height and block
    80  		// index: 4.1, 3.2, 3.1, 2.2...
    81  		// The resulting query should already sort the rows in the right order.
    82  		limit = first
    83  		sortOrder = "desc"
    84  	} else if last > 0 {
    85  		// We want the N oldest transactions, ascending on block height and block
    86  		// index: 1.1, 1.2, 2.1, 2.2...
    87  		// The resulting query should sort the rows in the chronological order. But
    88  		// that's necessary to apply the LIMIT clause. It will be sorted in the
    89  		// reverse chronological order later on.
    90  		limit = last
    91  		sortOrder = "asc"
    92  	}
    93  
    94  	if before != nil {
    95  		block := nextBindVar(&args, before.BlockNumber)
    96  		index := nextBindVar(&args, before.TxIndex)
    97  		predicate := fmt.Sprintf("(t.block_height, t.index) < (%s, %s)", block, index)
    98  		predicates = append(predicates, predicate)
    99  		// We change the sorting order because we want the transactions right before
   100  		// the cursor, meaning older transactions.
   101  		sortOrder = "desc"
   102  	}
   103  	if after != nil {
   104  		block := nextBindVar(&args, after.BlockNumber)
   105  		index := nextBindVar(&args, after.TxIndex)
   106  		predicate := fmt.Sprintf("(t.block_height, t.index) > (%s, %s)", block, index)
   107  		predicates = append(predicates, predicate)
   108  		// We change the sorting order because we want the transactions right after
   109  		// the cursor, meaning newer transaction. That's necessary to apply the
   110  		// LIMIT clause.
   111  		sortOrder = "asc"
   112  	}
   113  
   114  	if len(cmdTypes) > 0 {
   115  		predicates = append(predicates, fmt.Sprintf("t.cmd_type = ANY(%s)", nextBindVar(&args, cmdTypes)))
   116  	}
   117  
   118  	if len(exclCmdTypes) > 0 {
   119  		predicates = append(predicates, fmt.Sprintf("t.cmd_type != ALL(%s)", nextBindVar(&args, exclCmdTypes)))
   120  	}
   121  
   122  	if len(parties) > 0 {
   123  		partiesBytes := make([][]byte, len(parties))
   124  		for i, p := range parties {
   125  			partiesBytes[i] = []byte(p)
   126  		}
   127  		predicates = append(predicates, fmt.Sprintf("t.submitter = ANY(%s)", nextBindVar(&args, partiesBytes)))
   128  	}
   129  
   130  	for key, value := range filters {
   131  		var predicate string
   132  
   133  		if key == "tx.submitter" {
   134  			// tx.submitter is lifted out of attributes and into tx_results by a trigger for faster access
   135  			predicate = fmt.Sprintf("t.submitter= %s", nextBindVar(&args, value))
   136  		} else if key == "cmd.type" {
   137  			predicate = fmt.Sprintf("t.cmd_type= %s", nextBindVar(&args, value))
   138  		} else if key == "block.height" {
   139  			// much quicker to filter block height by joining to the block table than looking in attributes
   140  			predicate = fmt.Sprintf("t.block_height = %s", nextBindVar(&args, value))
   141  		} else {
   142  			predicate = fmt.Sprintf(`
   143  				EXISTS (SELECT 1 FROM events e JOIN attributes a ON e.rowid = a.event_id
   144  						WHERE e.tx_id = t.rowid
   145  						AND a.composite_key = %s
   146  						AND a.value = %s)`, nextBindVar(&args, key), nextBindVar(&args, value))
   147  		}
   148  		predicates = append(predicates, predicate)
   149  	}
   150  
   151  	if len(predicates) > 0 {
   152  		query = fmt.Sprintf("%s WHERE %s", query, strings.Join(predicates, " AND "))
   153  	}
   154  
   155  	query = fmt.Sprintf("%s ORDER BY t.block_height %s, t.index %s", query, sortOrder, sortOrder)
   156  	if limit != 0 {
   157  		query = fmt.Sprintf("%s LIMIT %d", query, limit)
   158  	}
   159  
   160  	var rows []entities.TxResultRow
   161  	if err := pgxscan.Select(ctx, s.pool, &rows, query, args...); err != nil {
   162  		return nil, fmt.Errorf("querying tx_results: %w", err)
   163  	}
   164  
   165  	txs := make([]*pb.Transaction, 0, len(rows))
   166  	for _, row := range rows {
   167  		tx, err := row.ToProto()
   168  		if err != nil {
   169  			s.log.Warn(fmt.Sprintf("unable to decode transaction %s: %v", row.TxHash, err))
   170  			continue
   171  		}
   172  		txs = append(txs, tx)
   173  	}
   174  
   175  	// Make sure the results are always order in the reverse chronological order,
   176  	// as required.
   177  	// This cannot be replaced by the `order by` in the request as it's used by the
   178  	// pagination system.
   179  	sort.Slice(txs, func(i, j int) bool {
   180  		if txs[i].Block == txs[j].Block {
   181  			return txs[i].Index > txs[j].Index
   182  		}
   183  		return txs[i].Block > txs[j].Block
   184  	})
   185  
   186  	return txs, nil
   187  }