code.vegaprotocol.io/vega@v0.79.0/datanode/sqlstore/ledger.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  	"io"
    22  	"strings"
    23  	"time"
    24  
    25  	"code.vegaprotocol.io/vega/datanode/entities"
    26  	"code.vegaprotocol.io/vega/datanode/metrics"
    27  	"code.vegaprotocol.io/vega/libs/ptr"
    28  	"code.vegaprotocol.io/vega/logging"
    29  	v2 "code.vegaprotocol.io/vega/protos/data-node/api/v2"
    30  
    31  	"github.com/georgysavva/scany/pgxscan"
    32  )
    33  
    34  var aggregateLedgerEntriesOrdering = TableOrdering{
    35  	ColumnOrdering{Name: "vega_time", Sorting: ASC},
    36  }
    37  
    38  const (
    39  	LedgerMaxDays = 5 * 24 * time.Hour
    40  )
    41  
    42  type Ledger struct {
    43  	*ConnectionSource
    44  	batcher ListBatcher[entities.LedgerEntry]
    45  	pending []entities.LedgerEntry
    46  }
    47  
    48  func NewLedger(connectionSource *ConnectionSource) *Ledger {
    49  	a := &Ledger{
    50  		ConnectionSource: connectionSource,
    51  		batcher:          NewListBatcher[entities.LedgerEntry]("ledger", entities.LedgerEntryColumns),
    52  	}
    53  	return a
    54  }
    55  
    56  func (ls *Ledger) Flush(ctx context.Context) ([]entities.LedgerEntry, error) {
    57  	defer metrics.StartSQLQuery("Ledger", "Flush")()
    58  
    59  	// This creates an entry time for the ledger entry that is guaranteed to be unique
    60  	// Block event sequence number cannot be used as multiple ledger entries can be created
    61  	// as the result of a single transfer event.
    62  	for i, le := range ls.pending {
    63  		le.LedgerEntryTime = entities.CreateLedgerEntryTime(le.VegaTime, i)
    64  		ls.batcher.Add(le)
    65  	}
    66  
    67  	ls.pending = nil
    68  
    69  	return ls.batcher.Flush(ctx, ls.ConnectionSource)
    70  }
    71  
    72  func (ls *Ledger) Add(le entities.LedgerEntry) error {
    73  	ls.pending = append(ls.pending, le)
    74  	return nil
    75  }
    76  
    77  func (ls *Ledger) GetByLedgerEntryTime(ctx context.Context, ledgerEntryTime time.Time) (entities.LedgerEntry, error) {
    78  	defer metrics.StartSQLQuery("Ledger", "GetByID")()
    79  	le := entities.LedgerEntry{}
    80  
    81  	return le, ls.wrapE(pgxscan.Get(ctx, ls.ConnectionSource, &le,
    82  		`SELECT ledger_entry_time, quantity, tx_hash, vega_time, transfer_time, type, account_from_balance, account_to_balance
    83  		 FROM ledger WHERE ledger_entry_time =$1`,
    84  		ledgerEntryTime))
    85  }
    86  
    87  func (ls *Ledger) GetAll(ctx context.Context) ([]entities.LedgerEntry, error) {
    88  	defer metrics.StartSQLQuery("Ledger", "GetAll")()
    89  	ledgerEntries := []entities.LedgerEntry{}
    90  	err := pgxscan.Select(ctx, ls.ConnectionSource, &ledgerEntries, `
    91  		SELECT ledger_entry_time, quantity, tx_hash, vega_time, transfer_time, type, account_from_balance, account_to_balance
    92  		FROM ledger`)
    93  	return ledgerEntries, err
    94  }
    95  
    96  func (ls *Ledger) GetByTxHash(ctx context.Context, txHash entities.TxHash) ([]entities.LedgerEntry, error) {
    97  	ledgerEntries := []entities.LedgerEntry{}
    98  	defer metrics.StartSQLQuery("Ledger", "GetByTxHash")()
    99  
   100  	err := pgxscan.Select(ctx, ls.ConnectionSource, &ledgerEntries, `
   101  		SELECT ledger_entry_time, account_from_id, account_to_id, quantity, tx_hash, vega_time, transfer_time, type, account_from_balance, account_to_balance
   102  		FROM ledger WHERE tx_hash=$1`,
   103  		txHash,
   104  	)
   105  	return ledgerEntries, err
   106  }
   107  
   108  // Return at most 5 days of ledger entries so that we don't have long-running queries that scan through
   109  // too much historic data in timescale to try and match the given date ranges.
   110  func (ls *Ledger) validateDateRange(dateRange entities.DateRange) entities.DateRange {
   111  	if dateRange.Start == nil && dateRange.End == nil {
   112  		return entities.DateRange{
   113  			Start: ptr.From(time.Now().Add(-LedgerMaxDays)),
   114  		}
   115  	}
   116  
   117  	if dateRange.Start == nil && dateRange.End != nil {
   118  		return entities.DateRange{
   119  			Start: ptr.From(dateRange.End.Add(-LedgerMaxDays)),
   120  			End:   dateRange.End,
   121  		}
   122  	}
   123  
   124  	if (dateRange.Start != nil && dateRange.End == nil) ||
   125  		(dateRange.Start != nil && dateRange.End != nil && dateRange.End.Sub(*dateRange.Start) > LedgerMaxDays) {
   126  		return entities.DateRange{
   127  			Start: dateRange.Start,
   128  			End:   ptr.From(dateRange.Start.Add(LedgerMaxDays)),
   129  		}
   130  	}
   131  
   132  	return dateRange
   133  }
   134  
   135  // Query requests and sums number of the ledger entries of a given subset of accounts, specified via the 'filter' argument.
   136  // It returns a time-series (implemented as a list of AggregateLedgerEntry structs), with a row for every time
   137  // the summed ledger entries of the set of specified accounts changes.
   138  // Listed queries should be limited to a single party from each side only. If no or more than one parties are provided
   139  // for sending and receiving accounts - the query returns error.
   140  //
   141  // Entries can be queried by:
   142  //   - listing ledger entries with filtering on the sending account (market_id, asset_id, account_type)
   143  //   - listing ledger entries with filtering on the receiving account (market_id, asset_id, account_type)
   144  //   - listing ledger entries with filtering on the sending AND receiving account
   145  //   - listing ledger entries with filtering on the transfer type (on top of above filters or as a standalone option)
   146  func (ls *Ledger) Query(
   147  	ctx context.Context,
   148  	filter *entities.LedgerEntryFilter,
   149  	dateRange entities.DateRange,
   150  	pagination entities.CursorPagination,
   151  ) (*[]entities.AggregatedLedgerEntry, entities.PageInfo, error) {
   152  	defer metrics.StartSQLQuery("Ledger", "Query")()
   153  
   154  	var (
   155  		pageInfo     entities.PageInfo
   156  		entries      []entities.AggregatedLedgerEntry
   157  		args         []interface{}
   158  		whereClauses []string
   159  	)
   160  
   161  	dateRange = ls.validateDateRange(dateRange)
   162  
   163  	if err := filterLedgerEntriesQuery(filter, &args, &whereClauses); err != nil {
   164  		return nil, pageInfo, fmt.Errorf("invalid filters: %w", err)
   165  	}
   166  
   167  	if dateRange.Start != nil {
   168  		whereClauses = append(whereClauses, fmt.Sprintf("ledger.ledger_entry_time >= %s", nextBindVar(&args, dateRange.Start.Format(time.RFC3339))))
   169  	}
   170  
   171  	if dateRange.End != nil {
   172  		whereClauses = append(whereClauses, fmt.Sprintf("ledger.ledger_entry_time < %s", nextBindVar(&args, dateRange.End.Format(time.RFC3339))))
   173  	}
   174  
   175  	if filter.TransferID != "" {
   176  		whereClauses = append(whereClauses, fmt.Sprintf("ledger.transfer_id = %s", nextBindVar(&args, filter.TransferID)))
   177  	}
   178  
   179  	query := fmt.Sprintf(`SELECT
   180  				ledger.quantity AS quantity,
   181  				ledger.type AS transfer_type,
   182  				ledger.account_from_balance AS from_account_balance,
   183  				ledger.account_to_balance AS to_account_balance,
   184  				account_from.asset_id AS asset_id,
   185  				account_from.party_id AS from_account_party_id,
   186  				account_from.market_id AS from_account_market_id,
   187  				account_from.type AS from_account_type,
   188  				account_to.party_id AS to_account_party_id,
   189  				account_to.market_id AS to_account_market_id,
   190  				account_to.type AS to_account_type,
   191  				ledger.ledger_entry_time AS vega_time,
   192  				ledger.transfer_id AS transfer_id
   193  			FROM ledger
   194  			INNER JOIN accounts AS account_from
   195  			ON ledger.account_from_id=account_from.id
   196  			INNER JOIN accounts AS account_to
   197  			ON ledger.account_to_id=account_to.id
   198  			WHERE %s`, strings.Join(whereClauses, " AND "),
   199  	)
   200  
   201  	// set prefix to ensure ordering on ledger times, assign to local variable to make it safe for concurrent use
   202  	ord := aggregateLedgerEntriesOrdering
   203  	ord.SetPrefixAll("ledger")
   204  	query, args, err := PaginateQuery[entities.AggregatedLedgerEntriesCursor](query, args, ord, pagination)
   205  	if err != nil {
   206  		return nil, pageInfo, err
   207  	}
   208  
   209  	if err := pgxscan.Select(ctx, ls.ConnectionSource, &entries, query, args...); err != nil {
   210  		return nil, pageInfo, err
   211  	}
   212  
   213  	entries, pageInfo = entities.PageEntities[*v2.AggregatedLedgerEntriesEdge](entries, pagination)
   214  
   215  	return &entries, pageInfo, nil
   216  }
   217  
   218  func (ls *Ledger) Export(
   219  	ctx context.Context,
   220  	partyID string,
   221  	assetID *string,
   222  	dateRange entities.DateRange,
   223  	writer io.Writer,
   224  ) error {
   225  	if partyID == "" {
   226  		return ErrLedgerEntryExportForParty
   227  	}
   228  
   229  	pid := entities.PartyID(partyID)
   230  	pidBytes, err := pid.Bytes()
   231  	if err != nil {
   232  		return fmt.Errorf("invalid party id: %w", err)
   233  	}
   234  
   235  	args := []any{pidBytes}
   236  	query := `
   237  		SELECT
   238  			l.vega_time,
   239  			l.quantity,
   240  			CASE
   241  				WHEN ta.party_id = $1 AND fa.party_id != $1 THEN quantity
   242  				WHEN fa.party_id = $1 AND ta.party_id != $1 THEN -quantity
   243  				ELSE 0
   244  			END AS effect,
   245  			l.type AS transfer_type,
   246  			ENCODE(fa.asset_id, 'hex') AS asset_id,
   247  			ENCODE(fa.market_id, 'hex') AS account_from_market_id,
   248  			CASE
   249  				WHEN fa.party_id='\x03' THEN 'network'
   250  				ELSE ENCODE(fa.party_id, 'hex')
   251  				END AS account_from_party_id,
   252  			CASE
   253  				WHEN fa.type=0 THEN 'UNSPECIFIED'
   254  				WHEN fa.type=1 THEN 'INSURANCE'
   255  				WHEN fa.type=2 THEN 'SETTLEMENT'
   256  				WHEN fa.type=3 THEN 'MARGIN'
   257  				WHEN fa.type=4 THEN 'GENERAL'
   258  				WHEN fa.type=5 THEN 'FEES_INFRASTRUCTURE'
   259  				WHEN fa.type=6 THEN 'FEES_LIQUIDITY'
   260  				WHEN fa.type=7 THEN 'FEES_MAKER'
   261  				WHEN fa.type=9 THEN 'BOND'
   262  				WHEN fa.type=10 THEN 'EXTERNAL'
   263  				WHEN fa.type=11 THEN 'GLOBAL_INSURANCE'
   264  				WHEN fa.type=12 THEN 'GLOBAL_REWARD'
   265  				WHEN fa.type=13 THEN 'PENDING_TRANSFERS'
   266  				WHEN fa.type=14 THEN 'REWARD_MAKER_PAID_FEES'
   267  				WHEN fa.type=15 THEN 'REWARD_MAKER_RECEIVED_FEES'
   268  				WHEN fa.type=16 THEN 'REWARD_LP_RECEIVED_FEES'
   269  				WHEN fa.type=17 THEN 'REWARD_MARKET_PROPOSERS'
   270  				WHEN fa.type=18 THEN 'HOLDING'
   271  				WHEN fa.type=19 THEN 'LP_LIQUIDITY_FEES'
   272  				WHEN fa.type=20 THEN 'LIQUIDITY_FEES_BONUS_DISTRIBUTION'
   273  				WHEN fa.type=21 THEN 'NETWORK_TREASURY'
   274  				WHEN fa.type=22 THEN 'VESTING_REWARDS'
   275  				WHEN fa.type=23 THEN 'VESTED_REWARDS'
   276  				WHEN fa.type=24 THEN 'REWARD_AVERAGE_POSITION'
   277  				WHEN fa.type=25 THEN 'REWARD_RELATIVE_RETURN'
   278  				WHEN fa.type=26 THEN 'REWARD_RETURN_VOLATILITY'
   279  				WHEN fa.type=27 THEN 'REWARD_VALIDATOR_RANKING'
   280  				WHEN fa.type=28 THEN 'PENDING_FEE_REFERRAL_REWARD'
   281  				WHEN fa.type=29 THEN 'ORDER_MARGIN'
   282  				ELSE 'UNKNOWN' END AS account_from_account_type,
   283  			l.account_from_balance AS account_from_balance,
   284  			ENCODE(ta.market_id, 'hex') AS account_to_market_id,
   285  			CASE
   286  				WHEN ta.party_id='\x03' THEN 'network'
   287  				ELSE ENCODE(ta.party_id, 'hex')
   288  				END AS account_to_party_id,
   289  			CASE
   290  				WHEN ta.type=0 THEN 'UNSPECIFIED'
   291  				WHEN ta.type=1 THEN 'INSURANCE'
   292  				WHEN ta.type=2 THEN 'SETTLEMENT'
   293  				WHEN ta.type=3 THEN 'MARGIN'
   294  				WHEN ta.type=4 THEN 'GENERAL'
   295  				WHEN ta.type=5 THEN 'FEES_INFRASTRUCTURE'
   296  				WHEN ta.type=6 THEN 'FEES_LIQUIDITY'
   297  				WHEN ta.type=7 THEN 'FEES_MAKER'
   298  				WHEN ta.type=9 THEN 'BOND'
   299  				WHEN ta.type=10 THEN 'EXTERNAL'
   300  				WHEN ta.type=11 THEN 'GLOBAL_INSURANCE'
   301  				WHEN ta.type=12 THEN 'GLOBAL_REWARD'
   302  				WHEN ta.type=13 THEN 'PENDING_TRANSFERS'
   303  				WHEN ta.type=14 THEN 'REWARD_MAKER_PAID_FEES'
   304  				WHEN ta.type=15 THEN 'REWARD_MAKER_RECEIVED_FEES'
   305  				WHEN ta.type=16 THEN 'REWARD_LP_RECEIVED_FEES'
   306  				WHEN ta.type=17 THEN 'REWARD_MARKET_PROPOSERS'
   307  				WHEN ta.type=18 THEN 'HOLDING'
   308  				WHEN ta.type=19 THEN 'LP_LIQUIDITY_FEES'
   309  				WHEN ta.type=20 THEN 'LIQUIDITY_FEES_BONUS_DISTRIBUTION'
   310  				WHEN ta.type=21 THEN 'NETWORK_TREASURY'
   311  				WHEN ta.type=22 THEN 'VESTING_REWARDS'
   312  				WHEN ta.type=23 THEN 'VESTED_REWARDS'
   313  				WHEN ta.type=24 THEN 'REWARD_AVERAGE_POSITION'
   314  				WHEN ta.type=25 THEN 'REWARD_RELATIVE_RETURN'
   315  				WHEN ta.type=26 THEN 'REWARD_RETURN_VOLATILITY'
   316  				WHEN ta.type=27 THEN 'REWARD_VALIDATOR_RANKING'
   317  				WHEN ta.type=28 THEN 'PENDING_FEE_REFERRAL_REWARD'
   318  				WHEN ta.type=29 THEN 'ORDER_MARGIN'
   319  				ELSE 'UNKNOWN' END AS account_to_account_type,
   320  			l.account_to_balance AS account_to_balance
   321  		FROM
   322  			ledger l
   323  			INNER JOIN accounts AS fa ON l.account_from_id=fa.id
   324  			INNER JOIN accounts AS ta ON l.account_to_id=ta.id
   325  
   326  		WHERE (ta.party_id = $1 OR fa.party_id = $1)
   327  		`
   328  
   329  	if assetID != nil {
   330  		id := entities.AssetID(*assetID)
   331  		idBytes, err := id.Bytes()
   332  		if err != nil {
   333  			return fmt.Errorf("invalid asset id: %w", err)
   334  		}
   335  		query = fmt.Sprintf("%s AND fa.asset_id = %s", query, nextBindVar(&args, idBytes))
   336  	}
   337  
   338  	if dateRange.Start != nil {
   339  		query = fmt.Sprintf("%s AND l.ledger_entry_time >= %s", query, nextBindVar(&args, dateRange.Start.Format(time.RFC3339)))
   340  	}
   341  
   342  	if dateRange.End != nil {
   343  		query = fmt.Sprintf("%s AND l.ledger_entry_time < %s", query, nextBindVar(&args, dateRange.End.Format(time.RFC3339)))
   344  	}
   345  
   346  	query = fmt.Sprintf("copy (%s ORDER BY l.ledger_entry_time) to STDOUT (FORMAT csv, HEADER)", query)
   347  
   348  	tag, err := ls.CopyTo(ctx, writer, query, args...)
   349  	if err != nil {
   350  		return fmt.Errorf("copying to stdout: %w", err)
   351  	}
   352  
   353  	ls.log.Debug("copy to CSV", logging.Int64("rows affected", tag.RowsAffected()))
   354  	return nil
   355  }