code.vegaprotocol.io/vega@v0.79.0/datanode/sqlstore/proposals.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 sqlstore
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"sort"
    22  	"strings"
    23  
    24  	"code.vegaprotocol.io/vega/datanode/entities"
    25  	"code.vegaprotocol.io/vega/datanode/metrics"
    26  	v2 "code.vegaprotocol.io/vega/protos/data-node/api/v2"
    27  
    28  	"github.com/georgysavva/scany/pgxscan"
    29  	"github.com/jackc/pgx/v4"
    30  )
    31  
    32  type Proposals struct {
    33  	*ConnectionSource
    34  }
    35  
    36  var proposalsOrdering = TableOrdering{
    37  	ColumnOrdering{Name: "vega_time", Sorting: ASC},
    38  	ColumnOrdering{Name: "id", Sorting: ASC},
    39  }
    40  
    41  func NewProposals(connectionSource *ConnectionSource) *Proposals {
    42  	p := &Proposals{
    43  		ConnectionSource: connectionSource,
    44  	}
    45  	return p
    46  }
    47  
    48  func (ps *Proposals) Add(ctx context.Context, p entities.Proposal) error {
    49  	defer metrics.StartSQLQuery("Proposals", "Add")()
    50  	_, err := ps.Exec(ctx,
    51  		`INSERT INTO proposals(
    52  			id,
    53  			batch_id,
    54  			reference,
    55  			party_id,
    56  			state,
    57  			terms,
    58  			batch_terms,
    59  			rationale,
    60  			reason,
    61  			error_details,
    62  			proposal_time,
    63  			vega_time,
    64  			required_majority,
    65  			required_participation,
    66  			required_lp_majority,
    67  			required_lp_participation,
    68  			tx_hash)
    69  		 VALUES ($1,  $2,  $3,  $4,  $5,  $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
    70  		 ON CONFLICT (id, vega_time) DO UPDATE SET
    71  			reference = EXCLUDED.reference,
    72  			party_id = EXCLUDED.party_id,
    73  			state = EXCLUDED.state,
    74  			terms = EXCLUDED.terms,
    75  			rationale = EXCLUDED.rationale,
    76  			reason = EXCLUDED.reason,
    77  			error_details = EXCLUDED.error_details,
    78  			proposal_time = EXCLUDED.proposal_time,
    79  			tx_hash = EXCLUDED.tx_hash
    80  			;
    81  		 `,
    82  		p.ID, p.BatchID, p.Reference, p.PartyID, p.State, p.Terms, p.BatchTerms, p.Rationale, p.Reason,
    83  		p.ErrorDetails, p.ProposalTime, p.VegaTime, p.RequiredMajority, p.RequiredParticipation,
    84  		p.RequiredLPMajority, p.RequiredLPParticipation, p.TxHash)
    85  	return err
    86  }
    87  
    88  func (ps *Proposals) getProposalsInBatch(ctx context.Context, batchID string) ([]entities.Proposal, error) {
    89  	var proposals []entities.Proposal
    90  	query := `SELECT * FROM proposals_current WHERE batch_id=$1`
    91  
    92  	rows, err := ps.Query(ctx, query, entities.ProposalID(batchID))
    93  	if err != nil {
    94  		return proposals, fmt.Errorf("querying proposals: %w", err)
    95  	}
    96  	defer rows.Close()
    97  
    98  	if err = pgxscan.ScanAll(&proposals, rows); err != nil {
    99  		return proposals, fmt.Errorf("parsing proposals: %w", err)
   100  	}
   101  
   102  	sort.Slice(proposals, func(i, j int) bool {
   103  		return proposals[i].Terms.EnactmentTimestamp < proposals[j].Terms.EnactmentTimestamp
   104  	})
   105  
   106  	return proposals, nil
   107  }
   108  
   109  // extendOrGetBatchProposal fetching sub proposals in case of batch proposal
   110  // or fetches the whole batch if sub proposal is requested.
   111  // If none of the above applies then proposal is returned without change.
   112  func (ps *Proposals) extendOrGetBatchProposal(ctx context.Context, p entities.Proposal) (entities.Proposal, error) {
   113  	// if proposal is part of batch fetch to whole batch
   114  	if p.BelongsToBatch() {
   115  		return ps.GetByID(ctx, p.BatchID.String())
   116  	}
   117  
   118  	// if it's batch fetch the sub proposals
   119  	if p.IsBatch() {
   120  		pps, err := ps.getProposalsInBatch(ctx, p.ID.String())
   121  		if err != nil {
   122  			return p, ps.wrapE(err)
   123  		}
   124  		p.Proposals = pps
   125  	}
   126  
   127  	return p, nil
   128  }
   129  
   130  func (ps *Proposals) GetByID(ctx context.Context, id string) (entities.Proposal, error) {
   131  	defer metrics.StartSQLQuery("Proposals", "GetByID")()
   132  	var p entities.Proposal
   133  	query := `SELECT * FROM proposals_current WHERE id=$1`
   134  
   135  	if err := pgxscan.Get(ctx, ps.ConnectionSource, &p, query, entities.ProposalID(id)); err != nil {
   136  		return p, ps.wrapE(pgxscan.Get(ctx, ps.ConnectionSource, &p, query, entities.ProposalID(id)))
   137  	}
   138  
   139  	p, err := ps.extendOrGetBatchProposal(ctx, p)
   140  	if err != nil {
   141  		return p, err
   142  	}
   143  
   144  	return p, nil
   145  }
   146  
   147  // GetByIDWithoutBatch returns a proposal without extending single proposal by fetching batch proposal.
   148  func (ps *Proposals) GetByIDWithoutBatch(ctx context.Context, id string) (entities.Proposal, error) {
   149  	defer metrics.StartSQLQuery("Proposals", "GetByIDWithoutBatch")()
   150  	var p entities.Proposal
   151  	query := `SELECT * FROM proposals_current WHERE id=$1`
   152  
   153  	if err := pgxscan.Get(ctx, ps.ConnectionSource, &p, query, entities.ProposalID(id)); err != nil {
   154  		return p, ps.wrapE(pgxscan.Get(ctx, ps.ConnectionSource, &p, query, entities.ProposalID(id)))
   155  	}
   156  
   157  	return p, nil
   158  }
   159  
   160  func (ps *Proposals) GetByReference(ctx context.Context, ref string) (entities.Proposal, error) {
   161  	defer metrics.StartSQLQuery("Proposals", "GetByReference")()
   162  	var p entities.Proposal
   163  	query := `SELECT * FROM proposals_current WHERE reference=$1 LIMIT 1`
   164  
   165  	if err := pgxscan.Get(ctx, ps.ConnectionSource, &p, query, ref); err != nil {
   166  		return p, ps.wrapE(err)
   167  	}
   168  
   169  	p, err := ps.extendOrGetBatchProposal(ctx, p)
   170  	if err != nil {
   171  		return p, err
   172  	}
   173  
   174  	return p, nil
   175  }
   176  
   177  func (ps *Proposals) GetByTxHash(ctx context.Context, txHash entities.TxHash) ([]entities.Proposal, error) {
   178  	defer metrics.StartSQLQuery("Proposals", "GetByTxHash")()
   179  
   180  	var proposals []entities.Proposal
   181  	query := `SELECT * FROM proposals WHERE tx_hash=$1`
   182  	err := pgxscan.Select(ctx, ps.ConnectionSource, &proposals, query, txHash)
   183  	if err != nil {
   184  		return nil, ps.wrapE(err)
   185  	}
   186  
   187  	for i, p := range proposals {
   188  		p, err := ps.extendOrGetBatchProposal(ctx, p)
   189  		if err != nil {
   190  			return proposals, err
   191  		}
   192  
   193  		proposals[i] = p
   194  	}
   195  
   196  	return proposals, nil
   197  }
   198  
   199  func getOpenStateProposalsQuery(inState *entities.ProposalState, conditions []string, pagination entities.CursorPagination,
   200  	pc *entities.ProposalCursor, pageForward bool, args ...interface{},
   201  ) (string, []interface{}, error) {
   202  	// if we're querying for a specific state and it's not the Open state,
   203  	// or if we are paging forward and the current state is not the open state
   204  	// then we do not need to query for any open state proposals
   205  	if (inState != nil && *inState != entities.ProposalStateOpen) ||
   206  		(pageForward && pc.State != entities.ProposalStateUnspecified && pc.State != entities.ProposalStateOpen) {
   207  		// we aren't interested in open proposals so the query should be empty
   208  		return "", args, nil
   209  	}
   210  
   211  	if pc.State != entities.ProposalStateOpen {
   212  		if pagination.HasForward() {
   213  			pagination.Forward.Cursor = nil
   214  		} else if pagination.HasBackward() {
   215  			pagination.Backward.Cursor = nil
   216  		}
   217  	}
   218  
   219  	conditions = append([]string{
   220  		fmt.Sprintf("state=%s", nextBindVar(&args, entities.ProposalStateOpen)),
   221  	}, conditions...)
   222  
   223  	query := `select * from proposals_current`
   224  
   225  	if len(conditions) > 0 {
   226  		query = fmt.Sprintf("%s WHERE %s", query, strings.Join(conditions, " AND "))
   227  	}
   228  
   229  	var err error
   230  	query, args, err = PaginateQuery[entities.ProposalCursor](query, args, proposalsOrdering, pagination)
   231  	if err != nil {
   232  		return "", args, err
   233  	}
   234  
   235  	return query, args, nil
   236  }
   237  
   238  func getOtherStateProposalsQuery(inState *entities.ProposalState, conditions []string, pagination entities.CursorPagination,
   239  	pc *entities.ProposalCursor, pageForward bool, args ...interface{},
   240  ) (string, []interface{}, error) {
   241  	// if we're filtering for state and the state is open,
   242  	// or we're paging forward, and the cursor has reached the open proposals
   243  	// then we don't need to return any non-open proposal results
   244  	if (inState != nil && *inState == entities.ProposalStateOpen) || (!pageForward && pc.State == entities.ProposalStateOpen) {
   245  		// the open state query should already be providing the correct query for this
   246  		return "", args, nil
   247  	}
   248  
   249  	if pagination.HasForward() {
   250  		if pc.State == entities.ProposalStateOpen || pc.State == entities.ProposalStateUnspecified {
   251  			pagination.Forward.Cursor = nil
   252  		}
   253  	} else if pagination.HasBackward() {
   254  		if pc.State == entities.ProposalStateOpen || pc.State == entities.ProposalStateUnspecified {
   255  			pagination.Backward.Cursor = nil
   256  		}
   257  	}
   258  
   259  	if inState == nil {
   260  		conditions = append([]string{
   261  			fmt.Sprintf("state!=%s", nextBindVar(&args, entities.ProposalStateOpen)),
   262  		}, conditions...)
   263  	} else {
   264  		conditions = append([]string{
   265  			fmt.Sprintf("state=%s", nextBindVar(&args, *inState)),
   266  		}, conditions...)
   267  	}
   268  	query := `select * from proposals_current`
   269  
   270  	if len(conditions) > 0 {
   271  		query = fmt.Sprintf("%s WHERE %s", query, strings.Join(conditions, " AND "))
   272  	}
   273  
   274  	var err error
   275  	query, args, err = PaginateQuery[entities.ProposalCursor](query, args, proposalsOrdering, pagination)
   276  	if err != nil {
   277  		return "", args, err
   278  	}
   279  	return query, args, nil
   280  }
   281  
   282  func clonePagination(p entities.CursorPagination) (entities.CursorPagination, error) {
   283  	var first, last int32
   284  	var after, before string
   285  
   286  	var pFirst, pLast *int32
   287  	var pAfter, pBefore *string
   288  
   289  	if p.HasForward() {
   290  		first = *p.Forward.Limit
   291  		pFirst = &first
   292  		if p.Forward.HasCursor() {
   293  			after = p.Forward.Cursor.Encode()
   294  			pAfter = &after
   295  		}
   296  	}
   297  
   298  	if p.HasBackward() {
   299  		last = *p.Backward.Limit
   300  		pLast = &last
   301  		if p.Backward.HasCursor() {
   302  			before = p.Backward.Cursor.Encode()
   303  			pBefore = &before
   304  		}
   305  	}
   306  
   307  	return entities.NewCursorPagination(pFirst, pAfter, pLast, pBefore, p.NewestFirst)
   308  }
   309  
   310  func (ps *Proposals) Get(ctx context.Context,
   311  	inState *entities.ProposalState,
   312  	partyIDStr *string,
   313  	proposalType *entities.ProposalType,
   314  	pagination entities.CursorPagination,
   315  ) ([]entities.Proposal, entities.PageInfo, error) {
   316  	// This one is a bit tricky because we want all the open proposals listed at the top, sorted by date
   317  	// then other proposals in date order regardless of state.
   318  
   319  	// In order to do this, we need to construct a union of proposals where state = open, order by vega_time
   320  	// and state != open, order by vega_time
   321  	// If the cursor has been set, and we're traversing forward (newest-oldest), then we need to check if the
   322  	// state of the cursor is = open. If it is then we should append the open state proposals with the non-open state
   323  	// proposals.
   324  	// If the cursor state is != open, we have navigated passed the open state proposals and only need the non-open state proposals.
   325  
   326  	// If the cursor has been set and we're traversing backward (newest-oldest), then we need to check if the
   327  	// state of the cursor is = open. If it is then we should only return the proposals where state = open as we've already navigated
   328  	// passed all the non-open proposals.
   329  	// if the state of the cursor is != open, then we need to append all the proposals where the state = open after the proposals where
   330  	// state != open.
   331  
   332  	// This combined results of both queries is then wrapped with another select which should return the appropriate number of rows that
   333  	// are required for the pagination to determine whether or not there are any next/previous rows for the pageInfo.
   334  	var (
   335  		pageInfo        entities.PageInfo
   336  		stateOpenQuery  string
   337  		stateOtherQuery string
   338  		stateOpenArgs   []interface{}
   339  		stateOtherArgs  []interface{}
   340  	)
   341  	args := make([]interface{}, 0)
   342  	cursor := extractCursorFromPagination(pagination)
   343  
   344  	pc := &entities.ProposalCursor{}
   345  
   346  	if cursor != "" {
   347  		err := pc.Parse(cursor)
   348  		if err != nil {
   349  			return nil, pageInfo, err
   350  		}
   351  	}
   352  
   353  	pageForward := pagination.HasForward() || (!pagination.HasForward() && !pagination.HasBackward())
   354  	var conditions []string
   355  
   356  	if partyIDStr != nil {
   357  		partyID := entities.PartyID(*partyIDStr)
   358  		conditions = append(conditions, fmt.Sprintf("party_id=%s", nextBindVar(&args, partyID)))
   359  	}
   360  
   361  	if proposalType != nil && *proposalType != entities.ProposalTypeAll {
   362  		conditions = append(conditions, fmt.Sprintf("terms ? %s", nextBindVar(&args, proposalType.String())))
   363  	}
   364  
   365  	var err error
   366  	var openPagination, otherPagination entities.CursorPagination
   367  	// we need to clone the pagination objects because we need to alter the pagination data for the different states
   368  	// to support the required ordering of the data
   369  	openPagination, err = clonePagination(pagination)
   370  	if err != nil {
   371  		return nil, pageInfo, fmt.Errorf("invalid pagination: %w", err)
   372  	}
   373  	otherPagination, err = clonePagination(pagination)
   374  	if err != nil {
   375  		return nil, pageInfo, fmt.Errorf("invalid pagination: %w", err)
   376  	}
   377  
   378  	stateOpenQuery, stateOpenArgs, err = getOpenStateProposalsQuery(inState, conditions, openPagination, pc, pageForward, args...)
   379  	if err != nil {
   380  		return nil, pageInfo, err
   381  	}
   382  	stateOtherQuery, stateOtherArgs, err = getOtherStateProposalsQuery(inState, conditions, otherPagination, pc, pageForward, args...)
   383  	if err != nil {
   384  		return nil, pageInfo, err
   385  	}
   386  
   387  	batch := &pgx.Batch{}
   388  
   389  	if stateOpenQuery != "" {
   390  		batch.Queue(stateOpenQuery, stateOpenArgs...)
   391  	}
   392  
   393  	if stateOtherQuery != "" {
   394  		batch.Queue(stateOtherQuery, stateOtherArgs...)
   395  	}
   396  
   397  	defer metrics.StartSQLQuery("Proposals", "Get")()
   398  	// copy the store connection because we may need to make recursive calls when processing the from the batch
   399  	// causing the underlying connection to be busy and unusable
   400  	batchConn := ps.ConnectionSource
   401  	results := batchConn.SendBatch(ctx, batch)
   402  	defer results.Close()
   403  
   404  	proposals := make([]entities.Proposal, 0)
   405  	fetchedBatches := map[entities.ProposalID]struct{}{}
   406  
   407  	for {
   408  		rows, err := results.Query()
   409  		if err != nil {
   410  			break
   411  		}
   412  
   413  		var matchedProps []entities.Proposal
   414  		if err := pgxscan.ScanAll(&matchedProps, rows); err != nil {
   415  			return nil, pageInfo, fmt.Errorf("querying proposals: %w", err)
   416  		}
   417  
   418  		rows.Close()
   419  
   420  		var props []entities.Proposal
   421  		for _, p := range matchedProps {
   422  			var batchID entities.ProposalID
   423  			if p.BelongsToBatch() {
   424  				batchID = p.BatchID
   425  			} else if p.IsBatch() {
   426  				batchID = p.ID
   427  			}
   428  
   429  			if _, ok := fetchedBatches[batchID]; ok {
   430  				continue
   431  			}
   432  
   433  			p, err := ps.extendOrGetBatchProposal(ctx, p)
   434  			if err != nil {
   435  				return nil, pageInfo, err
   436  			}
   437  			props = append(props, p)
   438  			fetchedBatches[p.ID] = struct{}{}
   439  		}
   440  
   441  		if pageForward {
   442  			proposals = append(proposals, props...)
   443  		} else {
   444  			proposals = append(props, proposals...)
   445  		}
   446  	}
   447  
   448  	if limit := calculateLimit(pagination); limit > 0 && limit < len(proposals) {
   449  		proposals = proposals[:limit]
   450  	}
   451  
   452  	proposals, pageInfo = entities.PageEntities[*v2.GovernanceDataEdge](proposals, pagination)
   453  	return proposals, pageInfo, nil
   454  }