code.vegaprotocol.io/vega@v0.79.0/core/staking/account.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 staking
    17  
    18  import (
    19  	"errors"
    20  	"sort"
    21  	"time"
    22  
    23  	"code.vegaprotocol.io/vega/core/types"
    24  	"code.vegaprotocol.io/vega/libs/num"
    25  )
    26  
    27  var (
    28  	ErrEventAlreadyExists = errors.New("event already exists")
    29  	ErrInvalidAmount      = errors.New("invalid amount")
    30  	ErrInvalidEventKind   = errors.New("invalid event kind")
    31  	ErrMissingEventID     = errors.New("missing event id")
    32  	ErrMissingTimestamp   = errors.New("missing timestamp")
    33  	ErrNegativeBalance    = errors.New("negative balance")
    34  	ErrInvalidParty       = errors.New("invalid party")
    35  )
    36  
    37  type Account struct {
    38  	Party   string
    39  	Balance *num.Uint
    40  	Events  []*types.StakeLinking
    41  }
    42  
    43  func NewStakingAccount(party string) *Account {
    44  	return &Account{
    45  		Party:   party,
    46  		Balance: num.UintZero(),
    47  		Events:  []*types.StakeLinking{},
    48  	}
    49  }
    50  
    51  func (s *Account) validateEvent(evt *types.StakeLinking) error {
    52  	if evt.Amount == nil || evt.Amount.IsZero() {
    53  		return ErrInvalidAmount
    54  	}
    55  	if evt.Type != types.StakeLinkingTypeDeposited && evt.Type != types.StakeLinkingTypeRemoved {
    56  		return ErrInvalidEventKind
    57  	}
    58  	if evt.TS <= 0 {
    59  		return ErrMissingTimestamp
    60  	}
    61  	if len(evt.ID) <= 0 {
    62  		return ErrMissingEventID
    63  	}
    64  	if evt.Party != s.Party {
    65  		return ErrInvalidParty
    66  	}
    67  
    68  	for _, v := range s.Events {
    69  		if evt.ID == v.ID {
    70  			return ErrEventAlreadyExists
    71  		}
    72  	}
    73  
    74  	return nil
    75  }
    76  
    77  // AddEvent will add a new event to the account.
    78  func (s *Account) AddEvent(evt *types.StakeLinking) error {
    79  	if err := s.validateEvent(evt); err != nil {
    80  		return err
    81  	}
    82  	// save the new events
    83  	s.insertSorted(evt)
    84  
    85  	// now update the ongoing balance
    86  	return s.computeOngoingBalance()
    87  }
    88  
    89  func (s *Account) GetAvailableBalance() *num.Uint {
    90  	return s.Balance.Clone()
    91  }
    92  
    93  func (s *Account) GetAvailableBalanceAt(at time.Time) (*num.Uint, error) {
    94  	atUnix := at.UnixNano()
    95  	return s.calculateBalance(func(evt *types.StakeLinking) bool {
    96  		return evt.TS <= atUnix
    97  	})
    98  }
    99  
   100  // GetAvailableBalanceInRange could return a negative balance
   101  // if some event are still expected to be received from the bridge.
   102  func (s *Account) GetAvailableBalanceInRange(from, to time.Time) (*num.Uint, error) {
   103  	// first compute the balance before the from time.
   104  	balance, err := s.GetAvailableBalanceAt(from)
   105  	if err != nil {
   106  		return num.UintZero(), err
   107  	}
   108  
   109  	minBalance := balance.Clone()
   110  
   111  	// now we have the balance at the from time.
   112  	// we will want to check how much was added / removed
   113  	// during the epoch, and make sure that the initial
   114  	// balance is still covered
   115  	var (
   116  		fromUnix = from.UnixNano()
   117  		toUnix   = to.UnixNano()
   118  	)
   119  	for i := 0; i < len(s.Events) && s.Events[i].TS <= toUnix; i++ {
   120  		if s.Events[i].TS > fromUnix {
   121  			evt := s.Events[i]
   122  			switch evt.Type {
   123  			case types.StakeLinkingTypeDeposited:
   124  				balance.AddSum(evt.Amount)
   125  			case types.StakeLinkingTypeRemoved:
   126  				if balance.LT(evt.Amount) {
   127  					return num.UintZero(), ErrNegativeBalance
   128  				}
   129  				balance.Sub(balance, evt.Amount)
   130  				minBalance = num.Min(balance, minBalance)
   131  			}
   132  		}
   133  	}
   134  
   135  	return minBalance, nil
   136  }
   137  
   138  // computeOnGoingBalance can return only 1 error which would
   139  // be ErrNegativeBalancem, while this sounds bad, it can happen
   140  // because of event being processed out of order but we can't
   141  // really prevent that, and would have to wait for the network
   142  // to have seen all events before getting a positive balance.
   143  func (s *Account) computeOngoingBalance() error {
   144  	balance, err := s.calculateBalance(func(evt *types.StakeLinking) bool {
   145  		return true
   146  	})
   147  	s.Balance.Set(balance)
   148  	return err
   149  }
   150  
   151  func (s *Account) insertSorted(evt *types.StakeLinking) {
   152  	s.Events = append(s.Events, evt)
   153  	// sort anyway, but we would expect the events to come in a sorted manner
   154  	sort.SliceStable(s.Events, func(i, j int) bool {
   155  		// check if timestamps are the same
   156  		if s.Events[i].TS == s.Events[j].TS {
   157  			// now we want to put deposit first to avoid any remove
   158  			// event before a withdraw
   159  			if s.Events[i].Type == types.StakeLinkingTypeRemoved && s.Events[j].Type == types.StakeLinkingTypeDeposited {
   160  				// we return false so they can switched
   161  				return false
   162  			}
   163  			// any other case is find to be as they are
   164  			return true
   165  		}
   166  
   167  		return s.Events[i].TS < s.Events[j].TS
   168  	})
   169  }
   170  
   171  type timeFilter func(*types.StakeLinking) bool
   172  
   173  func (s *Account) calculateBalance(f timeFilter) (*num.Uint, error) {
   174  	balance := num.UintZero()
   175  	for _, evt := range s.Events {
   176  		if f(evt) {
   177  			switch evt.Type {
   178  			case types.StakeLinkingTypeDeposited:
   179  				balance.Add(balance, evt.Amount)
   180  			case types.StakeLinkingTypeRemoved:
   181  				if balance.LT(evt.Amount) {
   182  					return num.UintZero(), ErrNegativeBalance
   183  				}
   184  				balance.Sub(balance, evt.Amount)
   185  			}
   186  		}
   187  	}
   188  	return balance, nil
   189  }