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 }