code.vegaprotocol.io/vega@v0.79.0/core/banking/oneoff_transfers_test.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 banking_test
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"testing"
    22  	"time"
    23  
    24  	"code.vegaprotocol.io/vega/core/assets"
    25  	"code.vegaprotocol.io/vega/core/banking"
    26  	"code.vegaprotocol.io/vega/core/types"
    27  	"code.vegaprotocol.io/vega/libs/num"
    28  
    29  	"github.com/golang/mock/gomock"
    30  	"github.com/stretchr/testify/assert"
    31  )
    32  
    33  func TestTransfers(t *testing.T) {
    34  	t.Run("invalid transfer kind", testInvalidTransferKind)
    35  	t.Run("onefoff not enough funds to transfer", testOneOffTransferNotEnoughFundsToTransfer)
    36  	t.Run("onefoff invalid transfers", testOneOffTransferInvalidTransfers)
    37  	t.Run("valid oneoff transfer", testValidOneOffTransfer)
    38  	t.Run("valid staking transfers", testStakingTransfers)
    39  	t.Run("valid oneoff with deliverOn", testValidOneOffTransferWithDeliverOn)
    40  	t.Run("valid oneoff with deliverOn in the past is done straight away", testValidOneOffTransferWithDeliverOnInThePastStraightAway)
    41  	t.Run("rejected if doesn't reach minimal amount", testRejectedIfDoesntReachMinimalAmount)
    42  	t.Run("valid oneoff transfer from derived key", testValidOneOffTransferWithFromDerivedKey)
    43  	t.Run("onefoff invalid transfers from derived key", testOneOffTransferInvalidTransfersWithFromDerivedKey)
    44  	t.Run("onefoff invalid owner transfers from derived key", testOneOffTransferInvalidOwnerTransfersWithFromDerivedKey)
    45  }
    46  
    47  func testRejectedIfDoesntReachMinimalAmount(t *testing.T) {
    48  	e := getTestEngine(t)
    49  
    50  	ctx := context.Background()
    51  	transfer := &types.TransferFunds{
    52  		Kind: types.TransferCommandKindOneOff,
    53  		OneOff: &types.OneOffTransfer{
    54  			TransferBase: &types.TransferBase{
    55  				From:            "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
    56  				FromAccountType: types.AccountTypeGeneral,
    57  				To:              "2e05fd230f3c9f4eaf0bdc5bfb7ca0c9d00278afc44637aab60da76653d7ccf0",
    58  				ToAccountType:   types.AccountTypeGeneral,
    59  				Asset:           assetNameETH,
    60  				Amount:          num.NewUint(10),
    61  				Reference:       "someref",
    62  			},
    63  		},
    64  	}
    65  
    66  	e.OnMinTransferQuantumMultiple(context.Background(), num.DecimalFromFloat(1))
    67  	// asset exists
    68  	e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
    69  	e.broker.EXPECT().Send(gomock.Any()).Times(1)
    70  
    71  	assert.EqualError(t,
    72  		e.TransferFunds(ctx, transfer),
    73  		"could not transfer funds, less than minimal amount requested to transfer",
    74  	)
    75  }
    76  
    77  func testInvalidTransferKind(t *testing.T) {
    78  	e := getTestEngine(t)
    79  
    80  	ctx := context.Background()
    81  	transfer := &types.TransferFunds{
    82  		Kind: types.TransferCommandKind(-1),
    83  	}
    84  	assert.EqualError(t,
    85  		e.TransferFunds(ctx, transfer),
    86  		banking.ErrUnsupportedTransferKind.Error(),
    87  	)
    88  }
    89  
    90  func testOneOffTransferNotEnoughFundsToTransfer(t *testing.T) {
    91  	e := getTestEngine(t)
    92  
    93  	ctx := context.Background()
    94  	transfer := &types.TransferFunds{
    95  		Kind: types.TransferCommandKindOneOff,
    96  		OneOff: &types.OneOffTransfer{
    97  			TransferBase: &types.TransferBase{
    98  				From:            "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
    99  				FromAccountType: types.AccountTypeGeneral,
   100  				To:              "2e05fd230f3c9f4eaf0bdc5bfb7ca0c9d00278afc44637aab60da76653d7ccf0",
   101  				ToAccountType:   types.AccountTypeGeneral,
   102  				Asset:           assetNameETH,
   103  				Amount:          num.NewUint(10),
   104  				Reference:       "someref",
   105  			},
   106  		},
   107  	}
   108  
   109  	fromAcc := types.Account{
   110  		Balance: num.NewUint(1),
   111  	}
   112  
   113  	// asset exists
   114  	e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
   115  	e.col.EXPECT().GetPartyGeneralAccount(gomock.Any(), gomock.Any()).Times(1).Return(&fromAcc, nil)
   116  	e.broker.EXPECT().Send(gomock.Any()).Times(1)
   117  
   118  	assert.EqualError(t,
   119  		e.TransferFunds(ctx, transfer),
   120  		fmt.Errorf("could not pay the fee for transfer: %w", banking.ErrNotEnoughFundsToTransfer).Error(),
   121  	)
   122  }
   123  
   124  func testOneOffTransferInvalidTransfers(t *testing.T) {
   125  	e := getTestEngine(t)
   126  
   127  	ctx := context.Background()
   128  	transfer := types.TransferFunds{
   129  		Kind:   types.TransferCommandKindOneOff,
   130  		OneOff: &types.OneOffTransfer{},
   131  	}
   132  
   133  	transferBase := types.TransferBase{
   134  		From:            "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
   135  		FromAccountType: types.AccountTypeGeneral,
   136  		To:              "2e05fd230f3c9f4eaf0bdc5bfb7ca0c9d00278afc44637aab60da76653d7ccf0",
   137  		ToAccountType:   types.AccountTypeGeneral,
   138  		Asset:           assetNameETH,
   139  		Amount:          num.NewUint(10),
   140  		Reference:       "someref",
   141  	}
   142  
   143  	// asset exists
   144  	e.assets.EXPECT().Get(gomock.Any()).AnyTimes().Return(nil, nil)
   145  	var baseCpy types.TransferBase
   146  
   147  	t.Run("invalid from account", func(t *testing.T) {
   148  		e.broker.EXPECT().Send(gomock.Any()).Times(1)
   149  		baseCpy := transferBase
   150  		transfer.OneOff.TransferBase = &baseCpy
   151  		transfer.OneOff.From = ""
   152  		assert.EqualError(t,
   153  			e.TransferFunds(ctx, &transfer),
   154  			types.ErrInvalidFromAccount.Error(),
   155  		)
   156  	})
   157  
   158  	t.Run("invalid to account", func(t *testing.T) {
   159  		e.broker.EXPECT().Send(gomock.Any()).Times(1)
   160  		baseCpy = transferBase
   161  		transfer.OneOff.TransferBase = &baseCpy
   162  		transfer.OneOff.To = ""
   163  		assert.EqualError(t,
   164  			e.TransferFunds(ctx, &transfer),
   165  			types.ErrInvalidToAccount.Error(),
   166  		)
   167  	})
   168  
   169  	t.Run("unsupported from account type", func(t *testing.T) {
   170  		e.broker.EXPECT().Send(gomock.Any()).Times(1)
   171  		baseCpy = transferBase
   172  		transfer.OneOff.TransferBase = &baseCpy
   173  		transfer.OneOff.FromAccountType = types.AccountTypeBond
   174  		assert.EqualError(t,
   175  			e.TransferFunds(ctx, &transfer),
   176  			types.ErrUnsupportedFromAccountType.Error(),
   177  		)
   178  	})
   179  
   180  	t.Run("unsuported to account type", func(t *testing.T) {
   181  		e.broker.EXPECT().Send(gomock.Any()).Times(1)
   182  		baseCpy = transferBase
   183  		transfer.OneOff.TransferBase = &baseCpy
   184  		transfer.OneOff.ToAccountType = types.AccountTypeBond
   185  		assert.EqualError(t,
   186  			e.TransferFunds(ctx, &transfer),
   187  			types.ErrUnsupportedToAccountType.Error(),
   188  		)
   189  	})
   190  
   191  	t.Run("zero funds transfer", func(t *testing.T) {
   192  		e.broker.EXPECT().Send(gomock.Any()).Times(1)
   193  		baseCpy = transferBase
   194  		transfer.OneOff.TransferBase = &baseCpy
   195  		transfer.OneOff.Amount = num.UintZero()
   196  		assert.EqualError(t,
   197  			e.TransferFunds(ctx, &transfer),
   198  			types.ErrCannotTransferZeroFunds.Error(),
   199  		)
   200  	})
   201  }
   202  
   203  func testValidOneOffTransfer(t *testing.T) {
   204  	e := getTestEngine(t)
   205  
   206  	// let's do a massive fee, easy to test
   207  	e.OnTransferFeeFactorUpdate(context.Background(), num.NewDecimalFromFloat(1))
   208  
   209  	ctx := context.Background()
   210  	transfer := &types.TransferFunds{
   211  		Kind: types.TransferCommandKindOneOff,
   212  		OneOff: &types.OneOffTransfer{
   213  			TransferBase: &types.TransferBase{
   214  				From:            "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
   215  				FromAccountType: types.AccountTypeGeneral,
   216  				To:              "0000000000000000000000000000000000000000000000000000000000000000",
   217  				ToAccountType:   types.AccountTypeGlobalReward,
   218  				Asset:           assetNameETH,
   219  				Amount:          num.NewUint(10),
   220  				Reference:       "someref",
   221  			},
   222  		},
   223  	}
   224  
   225  	fromAcc := types.Account{
   226  		Balance: num.NewUint(100),
   227  	}
   228  
   229  	// asset exists
   230  	e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(
   231  		assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
   232  	e.col.EXPECT().GetPartyGeneralAccount(gomock.Any(), gomock.Any()).Times(1).Return(&fromAcc, nil)
   233  
   234  	// assert the calculation of fees and transfer request are correct
   235  	e.col.EXPECT().TransferFunds(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
   236  		func(ctx context.Context,
   237  			transfers []*types.Transfer,
   238  			accountTypes []types.AccountType,
   239  			references []string,
   240  			feeTransfers []*types.Transfer,
   241  			feeTransfersAccountTypes []types.AccountType,
   242  		) ([]*types.LedgerMovement, error,
   243  		) {
   244  			t.Run("ensure transfers are correct", func(t *testing.T) {
   245  				// transfer is done fully instantly, we should have 2 transfer
   246  				assert.Len(t, transfers, 2)
   247  				assert.Equal(t, transfers[0].Owner, "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301")
   248  				assert.Equal(t, transfers[0].Amount.Amount, num.NewUint(10))
   249  				assert.Equal(t, transfers[0].Amount.Asset, assetNameETH)
   250  				assert.Equal(t, transfers[1].Owner, "0000000000000000000000000000000000000000000000000000000000000000")
   251  				assert.Equal(t, transfers[1].Amount.Amount, num.NewUint(10))
   252  				assert.Equal(t, transfers[1].Amount.Asset, assetNameETH)
   253  
   254  				// 2 account types too
   255  				assert.Len(t, accountTypes, 2)
   256  				assert.Equal(t, accountTypes[0], types.AccountTypeGeneral)
   257  				assert.Equal(t, accountTypes[1], types.AccountTypeGlobalReward)
   258  			})
   259  
   260  			t.Run("ensure fee transfers are correct", func(t *testing.T) {
   261  				assert.Len(t, feeTransfers, 1)
   262  				assert.Equal(t, feeTransfers[0].Owner, "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301")
   263  				assert.Equal(t, feeTransfers[0].Amount.Amount, num.NewUint(10))
   264  				assert.Equal(t, feeTransfers[0].Amount.Asset, assetNameETH)
   265  
   266  				// then the fees account types
   267  				assert.Len(t, feeTransfersAccountTypes, 1)
   268  				assert.Equal(t, accountTypes[0], types.AccountTypeGeneral)
   269  			})
   270  			return nil, nil
   271  		})
   272  
   273  	e.broker.EXPECT().Send(gomock.Any()).Times(3)
   274  	assert.NoError(t, e.TransferFunds(ctx, transfer))
   275  }
   276  
   277  func testStakingTransfers(t *testing.T) {
   278  	e := getTestEngine(t)
   279  
   280  	// let's do a massive fee, easy to test
   281  	e.OnTransferFeeFactorUpdate(context.Background(), num.NewDecimalFromFloat(1))
   282  	e.OnStakingAsset(context.Background(), "ETH")
   283  
   284  	ctx := context.Background()
   285  
   286  	t.Run("cannot transfer to another pubkey lock_for_staking", func(t *testing.T) {
   287  		transfer := &types.TransferFunds{
   288  			Kind: types.TransferCommandKindOneOff,
   289  			OneOff: &types.OneOffTransfer{
   290  				TransferBase: &types.TransferBase{
   291  					From:            "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
   292  					FromAccountType: types.AccountTypeGeneral,
   293  					To:              "10ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
   294  					ToAccountType:   types.AccountTypeLockedForStaking,
   295  					Asset:           assetNameETH,
   296  					Amount:          num.NewUint(10),
   297  					Reference:       "someref",
   298  				},
   299  			},
   300  		}
   301  
   302  		// asset exists
   303  		e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(
   304  			assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
   305  		e.broker.EXPECT().Send(gomock.Any()).Times(1)
   306  		assert.EqualError(t, e.TransferFunds(ctx, transfer), "transfers to locked for staking allowed only from own general account")
   307  	})
   308  
   309  	t.Run("cannot transfer from lock_for_staking to another general account", func(t *testing.T) {
   310  		transfer := &types.TransferFunds{
   311  			Kind: types.TransferCommandKindOneOff,
   312  			OneOff: &types.OneOffTransfer{
   313  				TransferBase: &types.TransferBase{
   314  					From:            "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
   315  					FromAccountType: types.AccountTypeLockedForStaking,
   316  					To:              "10ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
   317  					ToAccountType:   types.AccountTypeGeneral,
   318  					Asset:           assetNameETH,
   319  					Amount:          num.NewUint(10),
   320  					Reference:       "someref",
   321  				},
   322  			},
   323  		}
   324  
   325  		// asset exists
   326  		e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(
   327  			assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
   328  		e.broker.EXPECT().Send(gomock.Any()).Times(1)
   329  		assert.EqualError(t, e.TransferFunds(ctx, transfer), "transfers from locked for staking allowed only to own general account")
   330  	})
   331  
   332  	t.Run("can only transfer from lock_for_staking to own general account", func(t *testing.T) {
   333  		transfer := &types.TransferFunds{
   334  			Kind: types.TransferCommandKindOneOff,
   335  			OneOff: &types.OneOffTransfer{
   336  				TransferBase: &types.TransferBase{
   337  					From:            "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
   338  					FromAccountType: types.AccountTypeLockedForStaking,
   339  					To:              "0000000000000000000000000000000000000000000000000000000000000000",
   340  					ToAccountType:   types.AccountTypeGlobalReward,
   341  					Asset:           assetNameETH,
   342  					Amount:          num.NewUint(10),
   343  					Reference:       "someref",
   344  				},
   345  			},
   346  		}
   347  
   348  		// asset exists
   349  		e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(
   350  			assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
   351  		e.broker.EXPECT().Send(gomock.Any()).Times(1)
   352  		assert.EqualError(t, e.TransferFunds(ctx, transfer), "can only transfer from locked for staking to general account")
   353  	})
   354  
   355  	t.Run("can transfer from general to locked_for_staking and emit stake deposited", func(t *testing.T) {
   356  		transfer := &types.TransferFunds{
   357  			Kind: types.TransferCommandKindOneOff,
   358  			OneOff: &types.OneOffTransfer{
   359  				TransferBase: &types.TransferBase{
   360  					From:            "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
   361  					FromAccountType: types.AccountTypeGeneral,
   362  					To:              "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
   363  					ToAccountType:   types.AccountTypeLockedForStaking,
   364  					Asset:           assetNameETH,
   365  					Amount:          num.NewUint(10),
   366  					Reference:       "someref",
   367  				},
   368  			},
   369  		}
   370  
   371  		fromAcc := types.Account{
   372  			Balance: num.NewUint(100),
   373  		}
   374  
   375  		// asset exists
   376  		e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(
   377  			assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
   378  		e.col.EXPECT().GetPartyGeneralAccount(gomock.Any(), gomock.Any()).Times(1).Return(&fromAcc, nil)
   379  
   380  		// assert the calculation of fees and transfer request are correct
   381  		e.col.EXPECT().TransferFunds(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
   382  
   383  		e.broker.EXPECT().Send(gomock.Any()).Times(4)
   384  
   385  		// expect a call to the stake accounting
   386  		e.stakeAccounting.EXPECT().AddEvent(gomock.Any(), gomock.Any()).Times(1).Do(
   387  			func(_ context.Context, evt *types.StakeLinking) {
   388  				assert.Equal(t, evt.Type, types.StakeLinkingTypeDeposited)
   389  			})
   390  		assert.NoError(t, e.TransferFunds(ctx, transfer))
   391  	})
   392  
   393  	t.Run("can transfer from locked_for_staking to general and emit stake removed", func(t *testing.T) {
   394  		transfer := &types.TransferFunds{
   395  			Kind: types.TransferCommandKindOneOff,
   396  			OneOff: &types.OneOffTransfer{
   397  				TransferBase: &types.TransferBase{
   398  					From:            "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
   399  					FromAccountType: types.AccountTypeLockedForStaking,
   400  					To:              "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
   401  					ToAccountType:   types.AccountTypeGeneral,
   402  					Asset:           assetNameETH,
   403  					Amount:          num.NewUint(10),
   404  					Reference:       "someref",
   405  				},
   406  			},
   407  		}
   408  
   409  		fromAcc := types.Account{
   410  			Balance: num.NewUint(100),
   411  		}
   412  
   413  		// asset exists
   414  		e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(
   415  			assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
   416  		e.col.EXPECT().GetPartyLockedForStaking(gomock.Any(), gomock.Any()).Times(1).Return(&fromAcc, nil)
   417  
   418  		// assert the calculation of fees and transfer request are correct
   419  		e.col.EXPECT().TransferFunds(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
   420  
   421  		e.broker.EXPECT().Send(gomock.Any()).Times(4)
   422  
   423  		// expect a call to the stake accounting
   424  		e.stakeAccounting.EXPECT().AddEvent(gomock.Any(), gomock.Any()).Times(1).Do(
   425  			func(_ context.Context, evt *types.StakeLinking) {
   426  				assert.Equal(t, evt.Type, types.StakeLinkingTypeRemoved)
   427  			})
   428  		assert.NoError(t, e.TransferFunds(ctx, transfer))
   429  	})
   430  
   431  	t.Run("can transfer from vested to general and emit stake removed", func(t *testing.T) {
   432  		transfer := &types.TransferFunds{
   433  			Kind: types.TransferCommandKindOneOff,
   434  			OneOff: &types.OneOffTransfer{
   435  				TransferBase: &types.TransferBase{
   436  					From:            "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
   437  					FromAccountType: types.AccountTypeVestedRewards,
   438  					To:              "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
   439  					ToAccountType:   types.AccountTypeGeneral,
   440  					Asset:           assetNameETH,
   441  					Amount:          num.NewUint(10),
   442  					Reference:       "someref",
   443  				},
   444  			},
   445  		}
   446  
   447  		fromAcc := types.Account{
   448  			Balance: num.NewUint(100),
   449  		}
   450  
   451  		// asset exists
   452  		e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(
   453  			assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
   454  		e.col.EXPECT().GetPartyVestedRewardAccount(gomock.Any(), gomock.Any()).Times(1).Return(&fromAcc, nil)
   455  
   456  		// assert the calculation of fees and transfer request are correct
   457  		e.col.EXPECT().TransferFunds(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
   458  
   459  		e.broker.EXPECT().Send(gomock.Any()).Times(4)
   460  
   461  		// expect a call to the stake accounting
   462  		e.stakeAccounting.EXPECT().AddEvent(gomock.Any(), gomock.Any()).Times(1).Do(
   463  			func(_ context.Context, evt *types.StakeLinking) {
   464  				assert.Equal(t, evt.Type, types.StakeLinkingTypeRemoved)
   465  			})
   466  		assert.NoError(t, e.TransferFunds(ctx, transfer))
   467  	})
   468  }
   469  
   470  func testValidOneOffTransferWithDeliverOnInThePastStraightAway(t *testing.T) {
   471  	e := getTestEngine(t)
   472  
   473  	// let's do a massive fee, easy to test
   474  	e.OnTransferFeeFactorUpdate(context.Background(), num.NewDecimalFromFloat(1))
   475  	e.OnTick(context.Background(), time.Unix(10, 0))
   476  
   477  	deliverOn := time.Unix(9, 0)
   478  	ctx := context.Background()
   479  	transfer := &types.TransferFunds{
   480  		Kind: types.TransferCommandKindOneOff,
   481  		OneOff: &types.OneOffTransfer{
   482  			TransferBase: &types.TransferBase{
   483  				From:            "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
   484  				FromAccountType: types.AccountTypeGeneral,
   485  				To:              "0000000000000000000000000000000000000000000000000000000000000000",
   486  				ToAccountType:   types.AccountTypeGlobalReward,
   487  				Asset:           assetNameETH,
   488  				Amount:          num.NewUint(10),
   489  				Reference:       "someref",
   490  			},
   491  			DeliverOn: &deliverOn,
   492  		},
   493  	}
   494  
   495  	fromAcc := types.Account{
   496  		Balance: num.NewUint(100),
   497  	}
   498  
   499  	// asset exists
   500  	e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
   501  	e.col.EXPECT().GetPartyGeneralAccount(gomock.Any(), gomock.Any()).Times(1).Return(&fromAcc, nil)
   502  
   503  	// assert the calculation of fees and transfer request are correct
   504  	e.col.EXPECT().TransferFunds(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
   505  		func(ctx context.Context,
   506  			transfers []*types.Transfer,
   507  			accountTypes []types.AccountType,
   508  			references []string,
   509  			feeTransfers []*types.Transfer,
   510  			feeTransfersAccountTypes []types.AccountType,
   511  		) ([]*types.LedgerMovement, error,
   512  		) {
   513  			t.Run("ensure transfers are correct", func(t *testing.T) {
   514  				// transfer is done fully instantly, we should have 2 transfer
   515  				assert.Len(t, transfers, 2)
   516  				assert.Equal(t, transfers[0].Owner, "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301")
   517  				assert.Equal(t, transfers[0].Amount.Amount, num.NewUint(10))
   518  				assert.Equal(t, transfers[0].Amount.Asset, assetNameETH)
   519  				assert.Equal(t, transfers[1].Owner, "0000000000000000000000000000000000000000000000000000000000000000")
   520  				assert.Equal(t, transfers[1].Amount.Amount, num.NewUint(10))
   521  				assert.Equal(t, transfers[1].Amount.Asset, assetNameETH)
   522  
   523  				// 2 account types too
   524  				assert.Len(t, accountTypes, 2)
   525  				assert.Equal(t, accountTypes[0], types.AccountTypeGeneral)
   526  				assert.Equal(t, accountTypes[1], types.AccountTypeGlobalReward)
   527  			})
   528  
   529  			t.Run("ensure fee transfers are correct", func(t *testing.T) {
   530  				assert.Len(t, feeTransfers, 1)
   531  				assert.Equal(t, feeTransfers[0].Owner, "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301")
   532  				assert.Equal(t, feeTransfers[0].Amount.Amount, num.NewUint(10))
   533  				assert.Equal(t, feeTransfers[0].Amount.Asset, assetNameETH)
   534  
   535  				// then the fees account types
   536  				assert.Len(t, feeTransfersAccountTypes, 1)
   537  				assert.Equal(t, accountTypes[0], types.AccountTypeGeneral)
   538  			})
   539  			return nil, nil
   540  		})
   541  
   542  	e.broker.EXPECT().Send(gomock.Any()).Times(3)
   543  	assert.NoError(t, e.TransferFunds(ctx, transfer))
   544  }
   545  
   546  func testValidOneOffTransferWithDeliverOn(t *testing.T) {
   547  	e := getTestEngine(t)
   548  
   549  	// let's do a massive fee, easy to test
   550  	e.OnTransferFeeFactorUpdate(context.Background(), num.NewDecimalFromFloat(1))
   551  	e.OnTick(context.Background(), time.Unix(10, 0))
   552  
   553  	deliverOn := time.Unix(12, 0)
   554  	ctx := context.Background()
   555  	transfer := &types.TransferFunds{
   556  		Kind: types.TransferCommandKindOneOff,
   557  		OneOff: &types.OneOffTransfer{
   558  			TransferBase: &types.TransferBase{
   559  				From:            "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
   560  				FromAccountType: types.AccountTypeGeneral,
   561  				To:              "0000000000000000000000000000000000000000000000000000000000000000",
   562  				ToAccountType:   types.AccountTypeGlobalReward,
   563  				Asset:           assetNameETH,
   564  				Amount:          num.NewUint(10),
   565  				Reference:       "someref",
   566  			},
   567  			DeliverOn: &deliverOn,
   568  		},
   569  	}
   570  
   571  	fromAcc := types.Account{
   572  		Balance: num.NewUint(100),
   573  	}
   574  
   575  	// asset exists
   576  	e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
   577  	e.col.EXPECT().GetPartyGeneralAccount(gomock.Any(), gomock.Any()).Times(1).Return(&fromAcc, nil)
   578  
   579  	// assert the calculation of fees and transfer request are correct
   580  	e.col.EXPECT().TransferFunds(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
   581  		func(ctx context.Context,
   582  			transfers []*types.Transfer,
   583  			accountTypes []types.AccountType,
   584  			references []string,
   585  			feeTransfers []*types.Transfer,
   586  			feeTransfersAccountTypes []types.AccountType,
   587  		) ([]*types.LedgerMovement, error,
   588  		) {
   589  			t.Run("ensure transfers are correct", func(t *testing.T) {
   590  				// transfer is done fully instantly, we should have 2 transfer
   591  				assert.Len(t, transfers, 1)
   592  				assert.Equal(t, transfers[0].Owner, "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301")
   593  				assert.Equal(t, transfers[0].Amount.Amount, num.NewUint(10))
   594  				assert.Equal(t, transfers[0].Amount.Asset, assetNameETH)
   595  
   596  				// 2 account types too
   597  				assert.Len(t, accountTypes, 1)
   598  				assert.Equal(t, accountTypes[0], types.AccountTypeGeneral)
   599  			})
   600  
   601  			t.Run("ensure fee transfers are correct", func(t *testing.T) {
   602  				assert.Len(t, feeTransfers, 1)
   603  				assert.Equal(t, feeTransfers[0].Owner, "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301")
   604  				assert.Equal(t, feeTransfers[0].Amount.Amount, num.NewUint(10))
   605  				assert.Equal(t, feeTransfers[0].Amount.Asset, assetNameETH)
   606  
   607  				// then the fees account types
   608  				assert.Len(t, feeTransfersAccountTypes, 1)
   609  				assert.Equal(t, accountTypes[0], types.AccountTypeGeneral)
   610  			})
   611  			return nil, nil
   612  		})
   613  
   614  	e.broker.EXPECT().Send(gomock.Any()).Times(3)
   615  	assert.NoError(t, e.TransferFunds(ctx, transfer))
   616  
   617  	e.OnTick(context.Background(), time.Unix(11, 0))
   618  
   619  	// assert the calculation of fees and transfer request are correct
   620  	e.broker.EXPECT().Send(gomock.Any()).AnyTimes()
   621  	e.col.EXPECT().TransferFunds(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
   622  		func(ctx context.Context,
   623  			transfers []*types.Transfer,
   624  			accountTypes []types.AccountType,
   625  			references []string,
   626  			feeTransfers []*types.Transfer,
   627  			feeTransfersAccountTypes []types.AccountType,
   628  		) ([]*types.LedgerMovement, error,
   629  		) {
   630  			t.Run("ensure transfers are correct", func(t *testing.T) {
   631  				// transfer is done fully instantly, we should have 2 transfer
   632  				assert.Equal(t, transfers[0].Owner, "0000000000000000000000000000000000000000000000000000000000000000")
   633  				assert.Equal(t, transfers[0].Amount.Amount, num.NewUint(10))
   634  				assert.Equal(t, transfers[0].Amount.Asset, assetNameETH)
   635  
   636  				// 1 account types too
   637  				assert.Len(t, accountTypes, 1)
   638  				assert.Equal(t, accountTypes[0], types.AccountTypeGlobalReward)
   639  			})
   640  
   641  			t.Run("ensure fee transfers are correct", func(t *testing.T) {
   642  				assert.Len(t, feeTransfers, 0)
   643  			})
   644  			return nil, nil
   645  		})
   646  
   647  	e.broker.EXPECT().SendBatch(gomock.Any()).AnyTimes()
   648  	e.OnTick(context.Background(), time.Unix(12, 0))
   649  }
   650  
   651  func testValidOneOffTransferWithFromDerivedKey(t *testing.T) {
   652  	e := getTestEngine(t)
   653  
   654  	// let's do a massive fee, easy to test
   655  	e.OnTransferFeeFactorUpdate(context.Background(), num.NewDecimalFromFloat(1))
   656  
   657  	partyKey := "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301"
   658  	derivedKey := "c84fbf3442a2a9f9ca87c9cefe686aed241ff49981dd8ce819dd532cd42a8427"
   659  	amount := num.NewUint(10)
   660  
   661  	ctx := context.Background()
   662  	transfer := &types.TransferFunds{
   663  		Kind: types.TransferCommandKindOneOff,
   664  		OneOff: &types.OneOffTransfer{
   665  			TransferBase: &types.TransferBase{
   666  				From:            partyKey,
   667  				FromDerivedKey:  &derivedKey,
   668  				FromAccountType: types.AccountTypeVestedRewards,
   669  				To:              partyKey,
   670  				ToAccountType:   types.AccountTypeGeneral,
   671  				Asset:           assetNameETH,
   672  				Amount:          amount,
   673  				Reference:       "someref",
   674  			},
   675  		},
   676  	}
   677  
   678  	// asset exists
   679  	e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(
   680  		assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
   681  
   682  	vestedAccount := types.Account{
   683  		Owner: derivedKey,
   684  		// The amount is the same as the transfer amount to ensure that no fee is charged for this type of transaction.
   685  		Balance: amount,
   686  		Asset:   assetNameETH,
   687  	}
   688  
   689  	e.col.EXPECT().GetPartyVestedRewardAccount(derivedKey, assetNameETH).Return(&vestedAccount, nil).Times(1)
   690  	e.parties.EXPECT().CheckDerivedKeyOwnership(types.PartyID(partyKey), derivedKey).Return(true).Times(1)
   691  
   692  	// assert the calculation of fees and transfer request are correct
   693  	e.col.EXPECT().TransferFunds(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
   694  		func(ctx context.Context,
   695  			transfers []*types.Transfer,
   696  			accountTypes []types.AccountType,
   697  			references []string,
   698  			feeTransfers []*types.Transfer,
   699  			feeTransfersAccountTypes []types.AccountType,
   700  		) ([]*types.LedgerMovement, error,
   701  		) {
   702  			t.Run("ensure transfers are correct", func(t *testing.T) {
   703  				// transfer is done fully instantly, we should have 2 transfer
   704  				assert.Len(t, transfers, 2)
   705  				assert.Equal(t, derivedKey, transfers[0].Owner)
   706  				assert.Equal(t, num.NewUint(10), transfers[0].Amount.Amount)
   707  				assert.Equal(t, assetNameETH, transfers[0].Amount.Asset)
   708  				assert.Equal(t, partyKey, transfers[1].Owner)
   709  				assert.Equal(t, transfers[1].Amount.Amount, num.NewUint(10))
   710  				assert.Equal(t, transfers[1].Amount.Asset, assetNameETH)
   711  
   712  				// 2 account types too
   713  				assert.Len(t, accountTypes, 2)
   714  				assert.Equal(t, accountTypes[0], types.AccountTypeVestedRewards)
   715  				assert.Equal(t, accountTypes[1], types.AccountTypeGeneral)
   716  			})
   717  
   718  			t.Run("ensure fee transfers are correct", func(t *testing.T) {
   719  				assert.Len(t, feeTransfers, 1)
   720  				assert.Equal(t, partyKey, feeTransfers[0].Owner)
   721  				assert.Equal(t, num.UintZero(), feeTransfers[0].Amount.Amount)
   722  				assert.Equal(t, assetNameETH, feeTransfers[0].Amount.Asset)
   723  
   724  				// then the fees account types
   725  				assert.Len(t, feeTransfersAccountTypes, 1)
   726  				assert.Equal(t, accountTypes[0], types.AccountTypeVestedRewards)
   727  			})
   728  			return nil, nil
   729  		})
   730  
   731  	e.broker.EXPECT().Send(gomock.Any()).Times(3)
   732  	assert.NoError(t, e.TransferFunds(ctx, transfer))
   733  }
   734  
   735  func testOneOffTransferInvalidTransfersWithFromDerivedKey(t *testing.T) {
   736  	e := getTestEngine(t)
   737  
   738  	ctx := context.Background()
   739  	transfer := types.TransferFunds{
   740  		Kind:   types.TransferCommandKindOneOff,
   741  		OneOff: &types.OneOffTransfer{},
   742  	}
   743  
   744  	partyKey := "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301"
   745  	derivedKey := "c84fbf3442a2a9f9ca87c9cefe686aed241ff49981dd8ce819dd532cd42a8427"
   746  
   747  	transferBase := types.TransferBase{
   748  		From:            partyKey,
   749  		FromDerivedKey:  &derivedKey,
   750  		FromAccountType: types.AccountTypeVestedRewards,
   751  		To:              partyKey,
   752  		ToAccountType:   types.AccountTypeGeneral,
   753  		Asset:           assetNameETH,
   754  		Amount:          num.NewUint(10),
   755  		Reference:       "someref",
   756  	}
   757  
   758  	// asset exists
   759  	e.assets.EXPECT().Get(gomock.Any()).AnyTimes().Return(nil, nil)
   760  	var baseCpy types.TransferBase
   761  
   762  	t.Run("invalid from account", func(t *testing.T) {
   763  		e.broker.EXPECT().Send(gomock.Any()).Times(1)
   764  		baseCpy := transferBase
   765  		transfer.OneOff.TransferBase = &baseCpy
   766  		transfer.OneOff.From = ""
   767  		assert.EqualError(t,
   768  			e.TransferFunds(ctx, &transfer),
   769  			types.ErrInvalidFromAccount.Error(),
   770  		)
   771  	})
   772  
   773  	t.Run("invalid to account", func(t *testing.T) {
   774  		e.broker.EXPECT().Send(gomock.Any()).Times(1)
   775  		baseCpy = transferBase
   776  		transfer.OneOff.TransferBase = &baseCpy
   777  		transfer.OneOff.To = ""
   778  		assert.EqualError(t,
   779  			e.TransferFunds(ctx, &transfer),
   780  			types.ErrInvalidToAccount.Error(),
   781  		)
   782  	})
   783  
   784  	t.Run("unsupported from account type", func(t *testing.T) {
   785  		e.broker.EXPECT().Send(gomock.Any()).Times(1)
   786  		baseCpy = transferBase
   787  		transfer.OneOff.TransferBase = &baseCpy
   788  		transfer.OneOff.FromAccountType = types.AccountTypeGeneral
   789  		assert.EqualError(t,
   790  			e.TransferFunds(ctx, &transfer),
   791  			types.ErrUnsupportedFromAccountType.Error(),
   792  		)
   793  	})
   794  
   795  	t.Run("unsuported to account type", func(t *testing.T) {
   796  		e.broker.EXPECT().Send(gomock.Any()).Times(1)
   797  		baseCpy = transferBase
   798  		transfer.OneOff.TransferBase = &baseCpy
   799  		transfer.OneOff.ToAccountType = types.AccountTypeVestedRewards
   800  		assert.EqualError(t,
   801  			e.TransferFunds(ctx, &transfer),
   802  			types.ErrUnsupportedToAccountType.Error(),
   803  		)
   804  	})
   805  
   806  	t.Run("zero funds transfer", func(t *testing.T) {
   807  		e.broker.EXPECT().Send(gomock.Any()).Times(1)
   808  		baseCpy = transferBase
   809  		transfer.OneOff.TransferBase = &baseCpy
   810  		transfer.OneOff.Amount = num.UintZero()
   811  		assert.EqualError(t,
   812  			e.TransferFunds(ctx, &transfer),
   813  			types.ErrCannotTransferZeroFunds.Error(),
   814  		)
   815  	})
   816  }
   817  
   818  func testOneOffTransferInvalidOwnerTransfersWithFromDerivedKey(t *testing.T) {
   819  	e := getTestEngine(t)
   820  
   821  	// let's do a massive fee, easy to test
   822  	e.OnTransferFeeFactorUpdate(context.Background(), num.NewDecimalFromFloat(1))
   823  
   824  	partyKey := "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301"
   825  	derivedKey := "c84fbf3442a2a9f9ca87c9cefe686aed241ff49981dd8ce819dd532cd42a8427"
   826  	amount := num.NewUint(10)
   827  
   828  	ctx := context.Background()
   829  	transfer := &types.TransferFunds{
   830  		Kind: types.TransferCommandKindOneOff,
   831  		OneOff: &types.OneOffTransfer{
   832  			TransferBase: &types.TransferBase{
   833  				From:            partyKey,
   834  				FromDerivedKey:  &derivedKey,
   835  				FromAccountType: types.AccountTypeVestedRewards,
   836  				To:              partyKey,
   837  				ToAccountType:   types.AccountTypeGeneral,
   838  				Asset:           assetNameETH,
   839  				Amount:          amount,
   840  				Reference:       "someref",
   841  			},
   842  		},
   843  	}
   844  
   845  	// asset exists
   846  	e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(
   847  		assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
   848  
   849  	e.parties.EXPECT().CheckDerivedKeyOwnership(types.PartyID(partyKey), derivedKey).Return(false).Times(1)
   850  
   851  	e.broker.EXPECT().Send(gomock.Any()).Times(1)
   852  	assert.ErrorContains(t, e.TransferFunds(ctx, transfer), "does not own derived key")
   853  }