code.vegaprotocol.io/vega@v0.79.0/core/integration/steps/transfers.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 steps
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"strconv"
    22  	"strings"
    23  	"time"
    24  
    25  	"code.vegaprotocol.io/vega/core/banking"
    26  	"code.vegaprotocol.io/vega/core/types"
    27  	"code.vegaprotocol.io/vega/libs/num"
    28  	proto "code.vegaprotocol.io/vega/protos/vega"
    29  
    30  	"github.com/cucumber/godog"
    31  )
    32  
    33  func PartiesAvailableFeeDiscounts(
    34  	engine *banking.Engine,
    35  	table *godog.Table,
    36  ) error {
    37  	errs := []error{}
    38  	for _, r := range parseTransferFeeDiscountTable(table) {
    39  		asset := r.MustStr("asset")
    40  		party := r.MustStr("party")
    41  		actual := engine.AvailableFeeDiscount(asset, party)
    42  		expected := r.MustStr("available discount")
    43  		if expected != actual.String() {
    44  			errs = append(errs, errors.New(r.MustStr("party")+" expected "+expected+" but got "+actual.String()))
    45  		}
    46  	}
    47  	if len(errs) > 0 {
    48  		return ErrStack(errs)
    49  	}
    50  	return nil
    51  }
    52  
    53  func parseTransferFeeDiscountTable(table *godog.Table) []RowWrapper {
    54  	return StrictParseTable(table, []string{
    55  		"party",
    56  		"asset",
    57  		"available discount",
    58  	}, []string{})
    59  }
    60  
    61  func PartiesSubmitTransfers(
    62  	engine *banking.Engine,
    63  	table *godog.Table,
    64  ) error {
    65  	errs := []error{}
    66  	for _, r := range parseOneOffTransferTable(table) {
    67  		transfer, _ := rowToOneOffTransfer(r)
    68  		err := engine.TransferFunds(context.Background(), &types.TransferFunds{
    69  			Kind:   types.TransferCommandKindOneOff,
    70  			OneOff: transfer,
    71  		})
    72  		if len(r.Str("error")) > 0 || err != nil {
    73  			expected := r.Str("error")
    74  			actual := ""
    75  			if err != nil {
    76  				actual = err.Error()
    77  			}
    78  			if expected != actual {
    79  				errs = append(errs, errors.New(r.MustStr("id")+" expected "+expected+" but got "+actual))
    80  			}
    81  		}
    82  	}
    83  	if len(errs) > 0 {
    84  		return ErrStack(errs)
    85  	}
    86  	return nil
    87  }
    88  
    89  func parseOneOffTransferTable(table *godog.Table) []RowWrapper {
    90  	return StrictParseTable(table, []string{
    91  		"id", "from", "from_account_type", "to", "to_account_type", "asset", "amount", "delivery_time",
    92  	}, []string{"market", "error"})
    93  }
    94  
    95  func rowToOneOffTransfer(r RowWrapper) (*types.OneOffTransfer, error) {
    96  	id := r.MustStr("id")
    97  	from := r.MustStr("from")
    98  	fromAccountType := r.MustStr("from_account_type")
    99  	fromAT := proto.AccountType_value[fromAccountType]
   100  	to := r.MustStr("to")
   101  	toAccuontType := r.MustStr("to_account_type")
   102  	toAT := proto.AccountType_value[toAccuontType]
   103  	asset := r.MustStr("asset")
   104  	amount := r.MustStr("amount")
   105  	amountUint, _ := num.UintFromString(amount, 10)
   106  	deliveryTime, err := time.Parse("2006-01-02T15:04:05Z", r.MustStr("delivery_time"))
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  
   111  	oneOff := &types.OneOffTransfer{
   112  		TransferBase: &types.TransferBase{
   113  			ID:              id,
   114  			From:            from,
   115  			FromAccountType: types.AccountType(fromAT),
   116  			To:              to,
   117  			ToAccountType:   types.AccountType(toAT),
   118  			Asset:           asset,
   119  			Amount:          amountUint,
   120  		},
   121  		DeliverOn: &deliveryTime,
   122  	}
   123  	return oneOff, nil
   124  }
   125  
   126  func PartiesSubmitRecurringTransfers(
   127  	engine *banking.Engine,
   128  	table *godog.Table,
   129  ) error {
   130  	errs := []error{}
   131  	for _, r := range parseRecurringTransferTable(table) {
   132  		transfer := rowToRecurringTransfer(r)
   133  		err := engine.TransferFunds(context.Background(), &types.TransferFunds{
   134  			Kind:      types.TransferCommandKindRecurring,
   135  			Recurring: transfer,
   136  		})
   137  		if len(r.Str("error")) > 0 || err != nil {
   138  			expected := r.Str("error")
   139  			actual := ""
   140  			if err != nil {
   141  				actual = err.Error()
   142  			}
   143  			if expected != actual {
   144  				errs = append(errs, errors.New(r.MustStr("id")+" expected "+expected+" but got "+actual))
   145  			}
   146  		}
   147  	}
   148  	if len(errs) > 0 {
   149  		return ErrStack(errs)
   150  	}
   151  	return nil
   152  }
   153  
   154  func parseRecurringTransferTable(table *godog.Table) []RowWrapper {
   155  	return StrictParseTable(table, []string{
   156  		"id", "from", "from_account_type", "to", "to_account_type", "asset", "amount", "start_epoch", "end_epoch", "factor",
   157  	}, []string{"metric", "metric_asset", "markets", "lock_period", "window_length", "entity_scope", "individual_scope", "teams", "ntop", "staking_requirement", "notional_requirement", "distribution_strategy", "ranks", "cap_reward_fee_multiple", "transfer_interval", "target_notional_volume", "eligible_keys", "error"})
   158  }
   159  
   160  func rowToRecurringTransfer(r RowWrapper) *types.RecurringTransfer {
   161  	id := r.MustStr("id")
   162  	from := r.MustStr("from")
   163  	fromAccountType := r.MustStr("from_account_type")
   164  	fromAT := proto.AccountType_value[fromAccountType]
   165  	to := r.MustStr("to")
   166  	toAccuontType := r.MustStr("to_account_type")
   167  	toAT := proto.AccountType_value[toAccuontType]
   168  	asset := r.MustStr("asset")
   169  	amount := r.MustStr("amount")
   170  	amountUint, _ := num.UintFromString(amount, 10)
   171  	startEpoch, _ := num.UintFromString(r.MustStr("start_epoch"), 10)
   172  	endEpoch := r.MustStr("end_epoch")
   173  	var endEpochPtr *uint64
   174  	if len(endEpoch) > 0 {
   175  		endEpochUint, _ := num.UintFromString(r.MustStr("end_epoch"), 10)
   176  		endEpochUint64 := endEpochUint.Uint64()
   177  		endEpochPtr = &endEpochUint64
   178  	}
   179  
   180  	var dispatchStrategy *proto.DispatchStrategy
   181  	if len(r.Str("metric")) > 0 {
   182  		mkts := strings.Split(r.MustStr("markets"), ",")
   183  		if len(mkts) == 1 && mkts[0] == "" {
   184  			mkts = []string{}
   185  		}
   186  		lockPeriod := uint64(1)
   187  		if r.HasColumn("lock_period") {
   188  			lockPeriod = r.U64("lock_period")
   189  		}
   190  		windowLength := uint64(1)
   191  		if r.HasColumn("window_length") {
   192  			windowLength = r.U64("window_length")
   193  		}
   194  
   195  		var transferInterval *int32
   196  		if r.HasColumn("transfer_interval") {
   197  			interval := r.I32("transfer_interval")
   198  			transferInterval = &interval
   199  		}
   200  
   201  		var eligibleKeys []string
   202  		if r.HasColumn("eligible_keys") {
   203  			eligibleKeys = r.StrSlice("eligible_keys", ",")
   204  		}
   205  		var targetNotionalVolume *string
   206  		if r.HasColumn("target_notional_volume") {
   207  			tnv := r.Str("target_notional_volume")
   208  			targetNotionalVolume = &tnv
   209  		}
   210  
   211  		distributionStrategy := proto.DistributionStrategy_DISTRIBUTION_STRATEGY_PRO_RATA
   212  		var ranks []*proto.Rank
   213  		if r.HasColumn("distribution_strategy") {
   214  			distStrat := r.Str("distribution_strategy")
   215  			if distStrat == "PRO_RATA" {
   216  				distributionStrategy = proto.DistributionStrategy_DISTRIBUTION_STRATEGY_PRO_RATA
   217  			} else if distStrat == "RANK" || distStrat == "RANK_LOTTERY" {
   218  				if distStrat == "RANK" {
   219  					distributionStrategy = proto.DistributionStrategy_DISTRIBUTION_STRATEGY_RANK
   220  				} else {
   221  					distributionStrategy = proto.DistributionStrategy_DISTRIBUTION_STRATEGY_RANK_LOTTERY
   222  				}
   223  				rankList := strings.Split(r.MustStr("ranks"), ",")
   224  				ranks = make([]*proto.Rank, 0, len(rankList))
   225  				for _, r := range rankList {
   226  					rr := strings.Split(r, ":")
   227  					startRank, _ := strconv.ParseUint(rr[0], 10, 32)
   228  					shareRatio, _ := strconv.ParseUint(rr[1], 10, 32)
   229  					ranks = append(ranks, &proto.Rank{StartRank: uint32(startRank), ShareRatio: uint32(shareRatio)})
   230  				}
   231  			}
   232  		}
   233  
   234  		entityScope := proto.EntityScope_ENTITY_SCOPE_INDIVIDUALS
   235  		if r.HasColumn("entity_scope") {
   236  			scope := r.Str("entity_scope")
   237  			if scope == "INDIVIDUALS" {
   238  				entityScope = proto.EntityScope_ENTITY_SCOPE_INDIVIDUALS
   239  			} else if scope == "TEAMS" {
   240  				entityScope = proto.EntityScope_ENTITY_SCOPE_TEAMS
   241  			}
   242  		}
   243  
   244  		indiScope := proto.IndividualScope_INDIVIDUAL_SCOPE_UNSPECIFIED
   245  		if entityScope == proto.EntityScope_ENTITY_SCOPE_INDIVIDUALS {
   246  			indiScope = proto.IndividualScope_INDIVIDUAL_SCOPE_ALL
   247  			if r.HasColumn("individual_scope") {
   248  				indiScopeStr := r.Str("individual_scope")
   249  				if indiScopeStr == "ALL" {
   250  					indiScope = proto.IndividualScope_INDIVIDUAL_SCOPE_ALL
   251  				} else if indiScopeStr == "IN_TEAM" {
   252  					indiScope = proto.IndividualScope_INDIVIDUAL_SCOPE_IN_TEAM
   253  				} else if indiScopeStr == "NOT_IN_TEAM" {
   254  					indiScope = proto.IndividualScope_INDIVIDUAL_SCOPE_NOT_IN_TEAM
   255  				} else if indiScopeStr == "INDIVIDUAL_SCOPE_AMM" {
   256  					indiScope = proto.IndividualScope_INDIVIDUAL_SCOPE_AMM
   257  				}
   258  			}
   259  		}
   260  
   261  		teams := []string{}
   262  		ntop := ""
   263  		if entityScope == proto.EntityScope_ENTITY_SCOPE_TEAMS {
   264  			if r.HasColumn("teams") {
   265  				teams = strings.Split(r.MustStr("teams"), ",")
   266  				if len(teams) == 1 && teams[0] == "" {
   267  					teams = []string{}
   268  				}
   269  			}
   270  			ntop = r.MustStr("ntop")
   271  		}
   272  
   273  		stakingRequirement := ""
   274  		notionalRequirement := ""
   275  		capRewardFeeMultiple := ""
   276  		if r.HasColumn("staking_requirement") {
   277  			stakingRequirement = r.MustStr("staking_requirement")
   278  		}
   279  		if r.HasColumn("notional_requirement") {
   280  			notionalRequirement = r.mustColumn("notional_requirement")
   281  		}
   282  		if r.HasColumn("cap_reward_fee_multiple") {
   283  			capRewardFeeMultiple = r.MustStr("cap_reward_fee_multiple")
   284  		}
   285  
   286  		dispatchStrategy = &proto.DispatchStrategy{
   287  			AssetForMetric:       r.MustStr("metric_asset"),
   288  			Markets:              mkts,
   289  			Metric:               proto.DispatchMetric(proto.DispatchMetric_value[r.MustStr("metric")]),
   290  			DistributionStrategy: distributionStrategy,
   291  			LockPeriod:           lockPeriod,
   292  			EntityScope:          entityScope,
   293  			IndividualScope:      indiScope,
   294  			WindowLength:         windowLength,
   295  			TeamScope:            teams,
   296  			NTopPerformers:       ntop,
   297  			StakingRequirement:   stakingRequirement,
   298  			NotionalTimeWeightedAveragePositionRequirement: notionalRequirement,
   299  			RankTable:            ranks,
   300  			TransferInterval:     transferInterval,
   301  			EligibleKeys:         eligibleKeys,
   302  			TargetNotionalVolume: targetNotionalVolume,
   303  		}
   304  		if capRewardFeeMultiple != "" {
   305  			dispatchStrategy.CapRewardFeeMultiple = &capRewardFeeMultiple
   306  		}
   307  	}
   308  
   309  	factor := num.MustDecimalFromString(r.MustStr("factor"))
   310  	recurring := &types.RecurringTransfer{
   311  		TransferBase: &types.TransferBase{
   312  			ID:              id,
   313  			From:            from,
   314  			FromAccountType: types.AccountType(fromAT),
   315  			To:              to,
   316  			ToAccountType:   types.AccountType(toAT),
   317  			Asset:           asset,
   318  			Amount:          amountUint,
   319  		},
   320  		StartEpoch:       startEpoch.Uint64(),
   321  		EndEpoch:         endEpochPtr,
   322  		Factor:           factor,
   323  		DispatchStrategy: dispatchStrategy,
   324  	}
   325  	return recurring
   326  }
   327  
   328  func PartiesCancelTransfers(
   329  	engine *banking.Engine,
   330  	table *godog.Table,
   331  ) error {
   332  	errs := []error{}
   333  	for _, r := range parseOneOffCancellationTable(table) {
   334  		err := engine.CancelTransferFunds(context.Background(), &types.CancelTransferFunds{
   335  			Party:      r.MustStr("party"),
   336  			TransferID: r.MustStr("transfer_id"),
   337  		})
   338  		if len(r.Str("error")) > 0 || err != nil {
   339  			expected := r.Str("error")
   340  			actual := ""
   341  			if err != nil {
   342  				actual = err.Error()
   343  			}
   344  			if expected != actual {
   345  				errs = append(errs, errors.New(r.MustStr("transfer_id")+" expected "+expected+" but got "+actual))
   346  			}
   347  		}
   348  	}
   349  	if len(errs) > 0 {
   350  		return ErrStack(errs)
   351  	}
   352  	return nil
   353  }
   354  
   355  func parseOneOffCancellationTable(table *godog.Table) []RowWrapper {
   356  	return StrictParseTable(table, []string{
   357  		"party", "transfer_id",
   358  	}, []string{"error"})
   359  }