code.vegaprotocol.io/vega@v0.79.0/core/execution/spot/holding_account_tracker.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 spot
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"sort"
    22  
    23  	"code.vegaprotocol.io/vega/core/execution/common"
    24  	"code.vegaprotocol.io/vega/core/types"
    25  	"code.vegaprotocol.io/vega/libs/num"
    26  	"code.vegaprotocol.io/vega/libs/proto"
    27  	"code.vegaprotocol.io/vega/logging"
    28  
    29  	"golang.org/x/exp/maps"
    30  )
    31  
    32  type HoldingAccountTracker struct {
    33  	orderIDToQuantity map[string]*num.Uint
    34  	orderIDToFee      map[string]*num.Uint
    35  	collateral        common.Collateral
    36  	stopped           bool
    37  	log               *logging.Logger
    38  	snapshot          *types.PayloadHoldingAccountTracker
    39  }
    40  
    41  func NewHoldingAccountTracker(marketID string, log *logging.Logger, collateral common.Collateral) *HoldingAccountTracker {
    42  	return &HoldingAccountTracker{
    43  		orderIDToQuantity: map[string]*num.Uint{},
    44  		orderIDToFee:      map[string]*num.Uint{},
    45  		collateral:        collateral,
    46  		log:               log,
    47  		snapshot: &types.PayloadHoldingAccountTracker{
    48  			HoldingAccountTracker: &types.HoldingAccountTracker{
    49  				MarketID: marketID,
    50  			},
    51  		},
    52  	}
    53  }
    54  
    55  func (hat *HoldingAccountTracker) GetCurrentHolding(orderID string) (*num.Uint, *num.Uint) {
    56  	qty := num.UintZero()
    57  	fees := num.UintZero()
    58  	if q, ok := hat.orderIDToQuantity[orderID]; ok {
    59  		qty = q
    60  	}
    61  	if f, ok := hat.orderIDToFee[orderID]; ok {
    62  		fees = f
    63  	}
    64  	return qty, fees
    65  }
    66  
    67  func (hat *HoldingAccountTracker) TransferToHoldingAccount(ctx context.Context, orderID, party, asset string, quantity *num.Uint, fee *num.Uint, accountType types.AccountType) (*types.LedgerMovement, error) {
    68  	if _, ok := hat.orderIDToQuantity[orderID]; ok {
    69  		return nil, fmt.Errorf("funds for the order have already been transferred to the holding account")
    70  	}
    71  	total := num.Sum(quantity, fee)
    72  
    73  	transfer := &types.Transfer{
    74  		Owner: party,
    75  		Amount: &types.FinancialAmount{
    76  			Asset:  asset,
    77  			Amount: total,
    78  		},
    79  		Type: types.TransferTypeHoldingAccount,
    80  	}
    81  	le, err := hat.collateral.TransferToHoldingAccount(ctx, transfer, accountType)
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  	hat.orderIDToQuantity[orderID] = quantity
    86  	if !fee.IsZero() {
    87  		hat.orderIDToFee[orderID] = fee
    88  	}
    89  	return le, nil
    90  }
    91  
    92  func (hat *HoldingAccountTracker) TransferFeeToHoldingAccount(ctx context.Context, orderID, party, asset string, feeQuantity *num.Uint, fromAccountType types.AccountType) (*types.LedgerMovement, error) {
    93  	if feeQuantity.IsZero() {
    94  		return nil, nil
    95  	}
    96  	transfer := &types.Transfer{
    97  		Owner: party,
    98  		Amount: &types.FinancialAmount{
    99  			Asset:  asset,
   100  			Amount: feeQuantity.Clone(),
   101  		},
   102  		Type: types.TransferTypeHoldingAccount,
   103  	}
   104  	le, err := hat.collateral.TransferToHoldingAccount(ctx, transfer, fromAccountType)
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  	fee, ok := hat.orderIDToFee[orderID]
   109  	if ok {
   110  		hat.orderIDToFee[orderID].Add(feeQuantity, fee)
   111  	} else {
   112  		hat.orderIDToFee[orderID] = feeQuantity
   113  	}
   114  	return le, nil
   115  }
   116  
   117  func (hat *HoldingAccountTracker) ReleaseFeeFromHoldingAccount(ctx context.Context, orderID, party, asset string, toAccountType types.AccountType) (*types.LedgerMovement, error) {
   118  	feeQuantity, ok := hat.orderIDToFee[orderID]
   119  	if !ok {
   120  		return nil, fmt.Errorf("failed to find locked fee amount for order id %s", orderID)
   121  	}
   122  	transfer := &types.Transfer{
   123  		Owner: party,
   124  		Amount: &types.FinancialAmount{
   125  			Asset:  asset,
   126  			Amount: feeQuantity.Clone(),
   127  		},
   128  		Type: types.TransferTypeHoldingAccount,
   129  	}
   130  	delete(hat.orderIDToFee, orderID)
   131  	le, err := hat.collateral.ReleaseFromHoldingAccount(ctx, transfer, toAccountType)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	return le, err
   136  }
   137  
   138  func (hat *HoldingAccountTracker) ReleaseQuantityHoldingAccount(ctx context.Context, orderID, party, asset string, quantity *num.Uint, fee *num.Uint, toAccountType types.AccountType) (*types.LedgerMovement, error) {
   139  	total := num.Sum(quantity, fee)
   140  	if !fee.IsZero() {
   141  		lockedFee, ok := hat.orderIDToFee[orderID]
   142  		if !ok || lockedFee.LT(fee) {
   143  			return nil, fmt.Errorf("insufficient locked fee to release for order %s", orderID)
   144  		}
   145  	}
   146  	lockedQuantity, ok := hat.orderIDToQuantity[orderID]
   147  	if !ok || lockedQuantity.LT(quantity) {
   148  		return nil, fmt.Errorf("insufficient locked quantity to release for order %s", orderID)
   149  	}
   150  	if !fee.IsZero() {
   151  		hat.orderIDToFee[orderID] = num.UintZero().Sub(hat.orderIDToFee[orderID], fee)
   152  	}
   153  	hat.orderIDToQuantity[orderID] = num.UintZero().Sub(lockedQuantity, quantity)
   154  	transfer := &types.Transfer{
   155  		Owner: party,
   156  		Amount: &types.FinancialAmount{
   157  			Asset:  asset,
   158  			Amount: total,
   159  		},
   160  		Type: types.TransferTypeReleaseHoldingAccount,
   161  	}
   162  	le, err := hat.collateral.ReleaseFromHoldingAccount(ctx, transfer, toAccountType)
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  	return le, err
   167  }
   168  
   169  func (hat *HoldingAccountTracker) ReleaseQuantityHoldingAccountAuctionEnd(ctx context.Context, orderID, party, asset string, quantity *num.Uint, fee *num.Uint, toAccountType types.AccountType) (*types.LedgerMovement, error) {
   170  	effectiveFee := num.UintZero().Div(fee, num.NewUint(2))
   171  	total := num.Sum(quantity, effectiveFee)
   172  
   173  	if !effectiveFee.IsZero() {
   174  		lockedFee, ok := hat.orderIDToFee[orderID]
   175  		if !ok {
   176  			effectiveFee = num.UintZero()
   177  		} else {
   178  			effectiveFee = num.Min(effectiveFee, lockedFee)
   179  		}
   180  	}
   181  	lockedQuantity, ok := hat.orderIDToQuantity[orderID]
   182  	if !ok || lockedQuantity.LT(quantity) {
   183  		return nil, fmt.Errorf("insufficient locked quantity to release for order %s", orderID)
   184  	}
   185  	if !effectiveFee.IsZero() {
   186  		hat.orderIDToFee[orderID] = num.UintZero().Sub(hat.orderIDToFee[orderID], effectiveFee)
   187  	}
   188  	hat.orderIDToQuantity[orderID] = num.UintZero().Sub(lockedQuantity, quantity)
   189  	transfer := &types.Transfer{
   190  		Owner: party,
   191  		Amount: &types.FinancialAmount{
   192  			Asset:  asset,
   193  			Amount: total,
   194  		},
   195  		Type: types.TransferTypeReleaseHoldingAccount,
   196  	}
   197  	le, err := hat.collateral.ReleaseFromHoldingAccount(ctx, transfer, toAccountType)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  	return le, err
   202  }
   203  
   204  func (hat *HoldingAccountTracker) ReleaseAllFromHoldingAccount(ctx context.Context, orderID, party, asset string, toAccountType types.AccountType) (*types.LedgerMovement, error) {
   205  	fee := num.UintZero()
   206  	amt := num.UintZero()
   207  	if f, ok := hat.orderIDToFee[orderID]; ok {
   208  		fee = f
   209  	}
   210  	if a, ok := hat.orderIDToQuantity[orderID]; ok {
   211  		amt = a
   212  	}
   213  
   214  	total := num.Sum(fee, amt)
   215  	delete(hat.orderIDToFee, orderID)
   216  	delete(hat.orderIDToQuantity, orderID)
   217  
   218  	if total.IsZero() {
   219  		return nil, nil
   220  	}
   221  	transfer := &types.Transfer{
   222  		Owner: party,
   223  		Amount: &types.FinancialAmount{
   224  			Asset:  asset,
   225  			Amount: total,
   226  		},
   227  		Type: types.TransferTypeReleaseHoldingAccount,
   228  	}
   229  	return hat.collateral.ReleaseFromHoldingAccount(ctx, transfer, toAccountType)
   230  }
   231  
   232  func (hat *HoldingAccountTracker) StopSnapshots() {
   233  	hat.stopped = true
   234  }
   235  
   236  func (hat *HoldingAccountTracker) Keys() []string {
   237  	return []string{hat.snapshot.Key()}
   238  }
   239  
   240  func (hat *HoldingAccountTracker) Stopped() bool {
   241  	return hat.stopped
   242  }
   243  
   244  func (hat *HoldingAccountTracker) Namespace() types.SnapshotNamespace {
   245  	return types.HoldingAccountTrackerSnapshot
   246  }
   247  
   248  func (hat *HoldingAccountTracker) GetState(key string) ([]byte, []types.StateProvider, error) {
   249  	if key != hat.snapshot.Key() {
   250  		return nil, nil, types.ErrSnapshotKeyDoesNotExist
   251  	}
   252  
   253  	if hat.stopped {
   254  		return nil, nil, nil
   255  	}
   256  	payload := hat.buildPayload()
   257  
   258  	s, err := proto.Marshal(payload.IntoProto())
   259  	return s, nil, err
   260  }
   261  
   262  func (hat *HoldingAccountTracker) buildPayload() *types.Payload {
   263  	quantities := make([]*types.HoldingAccountQuantity, 0, len(hat.orderIDToQuantity))
   264  
   265  	orderIDs := map[string]struct{}{}
   266  	for k := range hat.orderIDToQuantity {
   267  		orderIDs[k] = struct{}{}
   268  	}
   269  	for k := range hat.orderIDToFee {
   270  		orderIDs[k] = struct{}{}
   271  	}
   272  	orderIDSlice := maps.Keys(orderIDs)
   273  	sort.Strings(orderIDSlice)
   274  
   275  	for _, oid := range orderIDSlice {
   276  		quantities = append(quantities, &types.HoldingAccountQuantity{
   277  			ID:          oid,
   278  			Quantity:    hat.orderIDToQuantity[oid],
   279  			FeeQuantity: hat.orderIDToFee[oid],
   280  		})
   281  	}
   282  
   283  	return &types.Payload{
   284  		Data: &types.PayloadHoldingAccountTracker{
   285  			HoldingAccountTracker: &types.HoldingAccountTracker{
   286  				MarketID:                 hat.snapshot.HoldingAccountTracker.MarketID,
   287  				HoldingAccountQuantities: quantities,
   288  			},
   289  		},
   290  	}
   291  }
   292  
   293  func (hat *HoldingAccountTracker) LoadState(_ context.Context, payload *types.Payload) ([]types.StateProvider, error) {
   294  	if hat.Namespace() != payload.Namespace() {
   295  		return nil, types.ErrInvalidSnapshotNamespace
   296  	}
   297  
   298  	var at *types.HoldingAccountTracker
   299  
   300  	switch pl := payload.Data.(type) {
   301  	case *types.PayloadHoldingAccountTracker:
   302  		at = pl.HoldingAccountTracker
   303  	default:
   304  		return nil, types.ErrUnknownSnapshotType
   305  	}
   306  
   307  	for _, haq := range at.HoldingAccountQuantities {
   308  		if haq.FeeQuantity != nil {
   309  			hat.orderIDToFee[haq.ID] = haq.FeeQuantity
   310  		}
   311  		if haq.Quantity != nil {
   312  			hat.orderIDToQuantity[haq.ID] = haq.Quantity
   313  		}
   314  	}
   315  
   316  	return nil, nil
   317  }