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 }