code.vegaprotocol.io/vega@v0.79.0/wallet/api/client_send_transaction_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 api_test 17 18 import ( 19 "context" 20 "errors" 21 "fmt" 22 "sync" 23 "testing" 24 "time" 25 26 "code.vegaprotocol.io/vega/libs/jsonrpc" 27 vgrand "code.vegaprotocol.io/vega/libs/rand" 28 apipb "code.vegaprotocol.io/vega/protos/vega/api/v1" 29 commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1" 30 "code.vegaprotocol.io/vega/wallet/api" 31 "code.vegaprotocol.io/vega/wallet/api/mocks" 32 nodemocks "code.vegaprotocol.io/vega/wallet/api/node/mocks" 33 "code.vegaprotocol.io/vega/wallet/api/node/types" 34 "code.vegaprotocol.io/vega/wallet/wallet" 35 36 "github.com/golang/mock/gomock" 37 "github.com/stretchr/testify/assert" 38 "github.com/stretchr/testify/require" 39 ) 40 41 func TestSendTransaction(t *testing.T) { 42 t.Run("Documentation matches the code", testClientSendTransactionSchemaCorrect) 43 t.Run("Sending a transaction with invalid params fails", testSendingTransactionWithInvalidParamsFails) 44 t.Run("Sending a transaction with valid params succeeds", testSendingTransactionWithValidParamsSucceeds) 45 t.Run("Sending a transaction in parallel blocks on same party but not on different parties", testSendingTransactionInParallelBlocksOnSamePartyButNotOnDifferentParties) 46 t.Run("Sending a transaction without the needed permissions send the transaction", testSendingTransactionWithoutNeededPermissionsDoesNotSendTransaction) 47 t.Run("Refusing the sending of a transaction does not send the transaction", testRefusingSendingOfTransactionDoesNotSendTransaction) 48 t.Run("Cancelling the review does not send the transaction", testCancellingTheReviewDoesNotSendTransaction) 49 t.Run("Interrupting the request does not send the transaction", testInterruptingTheRequestDoesNotSendTransaction) 50 t.Run("Getting internal error during the review does not send the transaction", testGettingInternalErrorDuringReviewDoesNotSendTransaction) 51 t.Run("No healthy node available does not send the transaction", testNoHealthyNodeAvailableDoesNotSendTransaction) 52 t.Run("Failing to get the spam statistics does not send the transaction", testFailingToGetSpamStatsDoesNotSendTransaction) 53 t.Run("Failure when sending transaction returns an error", testFailureWhenSendingTransactionReturnsAnError) 54 t.Run("Failing spam checks aborts the transaction", testFailingSpamChecksAbortsTheTransaction) 55 } 56 57 func testClientSendTransactionSchemaCorrect(t *testing.T) { 58 assertEqualSchema(t, "client.send_transaction", api.ClientSendTransactionParams{}, api.ClientSendTransactionResult{}) 59 } 60 61 func testSendingTransactionWithInvalidParamsFails(t *testing.T) { 62 tcs := []struct { 63 name string 64 params interface{} 65 expectedError error 66 }{ 67 { 68 name: "with nil params", 69 params: nil, 70 expectedError: api.ErrParamsRequired, 71 }, 72 { 73 name: "with wrong type of params", 74 params: "test", 75 expectedError: api.ErrParamsDoNotMatch, 76 }, 77 { 78 name: "with empty public key permissions", 79 params: api.ClientSendTransactionParams{ 80 PublicKey: "", 81 SendingMode: "TYPE_SYNC", 82 Transaction: testTransaction(t), 83 }, 84 expectedError: api.ErrPublicKeyIsRequired, 85 }, 86 { 87 name: "with empty sending mode", 88 params: api.ClientSendTransactionParams{ 89 PublicKey: vgrand.RandomStr(10), 90 SendingMode: "", 91 Transaction: testTransaction(t), 92 }, 93 expectedError: api.ErrSendingModeIsRequired, 94 }, 95 { 96 name: "with unsupported sending mode", 97 params: api.ClientSendTransactionParams{ 98 PublicKey: vgrand.RandomStr(10), 99 SendingMode: "TYPE_UNSPECIFIED", 100 Transaction: testTransaction(t), 101 }, 102 expectedError: api.ErrSendingModeCannotBeTypeUnspecified, 103 }, 104 { 105 name: "with unsupported sending mode", 106 params: api.ClientSendTransactionParams{ 107 PublicKey: vgrand.RandomStr(10), 108 SendingMode: "TYPE_MANY_FAST", 109 Transaction: testTransaction(t), 110 }, 111 expectedError: errors.New(`the sending mode "TYPE_MANY_FAST" is not a valid one`), 112 }, 113 { 114 name: "with no transaction", 115 params: api.ClientSendTransactionParams{ 116 PublicKey: vgrand.RandomStr(10), 117 SendingMode: "TYPE_SYNC", 118 Transaction: nil, 119 }, 120 expectedError: api.ErrTransactionIsRequired, 121 }, 122 { 123 name: "with transaction as invalid Vega command", 124 params: api.ClientSendTransactionParams{ 125 PublicKey: vgrand.RandomStr(10), 126 SendingMode: "TYPE_SYNC", 127 Transaction: map[string]interface{}{ 128 "type": "not vega command", 129 }, 130 }, 131 expectedError: errors.New("the transaction does not use a valid Vega command: unknown field \"type\" in vega.wallet.v1.SubmitTransactionRequest"), 132 }, 133 } 134 135 for _, tc := range tcs { 136 t.Run(tc.name, func(tt *testing.T) { 137 // given 138 ctx, _ := clientContextForTest() 139 hostname := vgrand.RandomStr(5) 140 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 141 PublicKeys: wallet.PublicKeysPermission{ 142 Access: wallet.ReadAccess, 143 AllowedKeys: nil, 144 }, 145 }) 146 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 147 if err != nil { 148 t.Fatalf(err.Error()) 149 } 150 151 // setup 152 handler := newSendTransactionHandler(tt) 153 154 // when 155 result, errorDetails := handler.handle(t, ctx, tc.params, connectedWallet) 156 157 // then 158 require.Empty(tt, result) 159 assertInvalidParams(tt, errorDetails, tc.expectedError) 160 }) 161 } 162 } 163 164 func testSendingTransactionWithValidParamsSucceeds(t *testing.T) { 165 // given 166 ctx, traceID := clientContextForTest() 167 hostname := vgrand.RandomStr(5) 168 nodeHost := vgrand.RandomStr(5) 169 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 170 PublicKeys: wallet.PublicKeysPermission{ 171 Access: wallet.ReadAccess, 172 AllowedKeys: nil, 173 }, 174 }) 175 kp, err := wallet1.GenerateKeyPair(nil) 176 if err != nil { 177 t.Fatalf(err.Error()) 178 } 179 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 180 if err != nil { 181 t.Fatalf(err.Error()) 182 } 183 txHash := vgrand.RandomStr(64) 184 spamStats := types.SpamStatistics{ 185 ChainID: vgrand.RandomStr(5), 186 LastBlockHeight: 100, 187 Proposals: &types.SpamStatistic{MaxForEpoch: 1}, 188 NodeAnnouncements: &types.SpamStatistic{MaxForEpoch: 1}, 189 Delegations: &types.SpamStatistic{MaxForEpoch: 1}, 190 Transfers: &types.SpamStatistic{MaxForEpoch: 1}, 191 Votes: &types.VoteSpamStatistics{MaxForEpoch: 1}, 192 PoW: &types.PoWStatistics{ 193 PowBlockStates: []types.PoWBlockState{{}}, 194 }, 195 } 196 197 // setup 198 handler := newSendTransactionHandler(t) 199 200 // -- expected calls 201 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 202 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, traceID).Times(1) 203 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 204 handler.interactor.EXPECT().RequestTransactionReviewForSending(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(true, nil) 205 handler.nodeSelector.EXPECT().Node(ctx, gomock.Any()).Times(1).Return(handler.node, nil) 206 handler.node.EXPECT().SpamStatistics(ctx, kp.PublicKey()).Times(1).Return(spamStats, nil) 207 handler.node.EXPECT().Host().Times(1).Return(nodeHost) 208 handler.spam.EXPECT().CheckSubmission(gomock.Any(), &spamStats).Times(1).Return(nil) 209 handler.spam.EXPECT().GenerateProofOfWork(kp.PublicKey(), gomock.Any()).Times(1).Return(&commandspb.ProofOfWork{ 210 Tid: vgrand.RandomStr(5), 211 Nonce: 12345678, 212 }, nil) 213 handler.node.EXPECT().SendTransaction(ctx, gomock.Any(), apipb.SubmitTransactionRequest_TYPE_SYNC).Times(1).Return(txHash, nil) 214 handler.interactor.EXPECT().NotifySuccessfulTransaction(ctx, traceID, uint8(2), txHash, gomock.Any(), gomock.Any(), gomock.Any(), nodeHost).Times(1) 215 handler.interactor.EXPECT().Log(ctx, traceID, gomock.Any(), gomock.Any()).AnyTimes() 216 217 // when 218 result, errorDetails := handler.handle(t, ctx, api.ClientSendTransactionParams{ 219 PublicKey: kp.PublicKey(), 220 SendingMode: "TYPE_SYNC", 221 Transaction: testTransaction(t), 222 }, connectedWallet) 223 224 // then 225 assert.Nil(t, errorDetails) 226 require.NotEmpty(t, result) 227 assert.Equal(t, txHash, result.TransactionHash) 228 assert.NotEmpty(t, result.Transaction) 229 } 230 231 func testSendingTransactionInParallelBlocksOnSamePartyButNotOnDifferentParties(t *testing.T) { 232 // setup 233 234 // Use channels to orchestrate requests. 235 sendSecondRequests := make(chan interface{}) 236 sendThirdRequests := make(chan interface{}) 237 waitForSecondRequestToExit := make(chan interface{}) 238 waitForThirdRequestToExit := make(chan interface{}) 239 240 hostname := vgrand.RandomStr(5) 241 nodeHost := vgrand.RandomStr(5) 242 243 // One context for each request. 244 r1Ctx, r1TraceID := clientContextForTest() 245 r2Ctx, _ := clientContextForTest() 246 r3Ctx, r3TraceID := clientContextForTest() 247 248 // A wallet with 2 keys to have 2 different parties. 249 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 250 PublicKeys: wallet.PublicKeysPermission{ 251 Access: wallet.ReadAccess, 252 AllowedKeys: nil, 253 }, 254 }) 255 kp1, err := wallet1.GenerateKeyPair(nil) 256 require.NoError(t, err) 257 kp2, err := wallet1.GenerateKeyPair(nil) 258 require.NoError(t, err) 259 260 // We can have a single connection as the implementation only cares about the 261 // party. 262 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 263 require.NoError(t, err) 264 265 // Some mock data. Their value is irrelevant to test parallelism, so we recycle 266 // them. 267 txHash := vgrand.RandomStr(64) 268 spamStats := types.SpamStatistics{ 269 ChainID: vgrand.RandomStr(5), 270 LastBlockHeight: 100, 271 Proposals: &types.SpamStatistic{MaxForEpoch: 1}, 272 NodeAnnouncements: &types.SpamStatistic{MaxForEpoch: 1}, 273 Delegations: &types.SpamStatistic{MaxForEpoch: 1}, 274 Transfers: &types.SpamStatistic{MaxForEpoch: 1}, 275 Votes: &types.VoteSpamStatistics{MaxForEpoch: 1}, 276 PoW: &types.PoWStatistics{ 277 PowBlockStates: []types.PoWBlockState{{}}, 278 }, 279 } 280 pow := &commandspb.ProofOfWork{ 281 Tid: vgrand.RandomStr(5), 282 Nonce: 12345678, 283 } 284 285 // Setting up the mocked calls. The second request shouldn't trigger any of 286 // them, since it should be rejected because it uses the same party as the 287 // first request, which only unblock at the end. 288 handler := newSendTransactionHandler(t) 289 290 gomock.InOrder( 291 // First request. 292 handler.spam.EXPECT().GenerateProofOfWork(kp1.PublicKey(), &spamStats).Times(1).Return(pow, nil), 293 // Third request. 294 handler.spam.EXPECT().GenerateProofOfWork(kp2.PublicKey(), &spamStats).Times(1).Return(pow, nil), 295 ) 296 gomock.InOrder( 297 // First request. 298 handler.interactor.EXPECT().NotifyInteractionSessionBegan(r1Ctx, r1TraceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil), 299 // Third request. 300 handler.interactor.EXPECT().NotifyInteractionSessionBegan(r3Ctx, r3TraceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil), 301 ) 302 gomock.InOrder( 303 // Third request is expected before because the first request get unblocked 304 // when the third request finishes. 305 handler.interactor.EXPECT().NotifyInteractionSessionEnded(r3Ctx, r3TraceID).Times(1), 306 // First request. 307 handler.interactor.EXPECT().NotifyInteractionSessionEnded(r1Ctx, r1TraceID).Times(1), 308 ) 309 gomock.InOrder( 310 // First request. 311 handler.interactor.EXPECT().RequestTransactionReviewForSending(r1Ctx, r1TraceID, uint8(1), hostname, wallet1.Name(), kp1.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(true, nil), 312 // Third request. 313 handler.interactor.EXPECT().RequestTransactionReviewForSending(r3Ctx, r3TraceID, uint8(1), hostname, wallet1.Name(), kp2.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(true, nil), 314 ) 315 gomock.InOrder( 316 // First request. 317 handler.nodeSelector.EXPECT().Node(r1Ctx, gomock.Any()).Times(1).Return(handler.node, nil), 318 // Third request. 319 handler.nodeSelector.EXPECT().Node(r3Ctx, gomock.Any()).Times(1).Return(handler.node, nil), 320 ) 321 gomock.InOrder( 322 // First request. 323 handler.node.EXPECT().SendTransaction(r1Ctx, gomock.Any(), apipb.SubmitTransactionRequest_TYPE_SYNC).Times(1).Return(txHash, nil), 324 // Third request. 325 handler.node.EXPECT().SendTransaction(r3Ctx, gomock.Any(), apipb.SubmitTransactionRequest_TYPE_SYNC).Times(1).Return(txHash, nil), 326 ) 327 gomock.InOrder( 328 // First request. 329 handler.walletStore.EXPECT().GetWallet(r1Ctx, wallet1.Name()).Times(1).Return(wallet1, nil), 330 // Second request. 331 handler.walletStore.EXPECT().GetWallet(r2Ctx, wallet1.Name()).Times(1).DoAndReturn(func(_ context.Context, _ string) (wallet.Wallet, error) { 332 close(sendThirdRequests) 333 return wallet1, nil 334 }), 335 // Third request. 336 handler.walletStore.EXPECT().GetWallet(r3Ctx, wallet1.Name()).Times(1).Return(wallet1, nil), 337 ) 338 gomock.InOrder( 339 // First request. 340 handler.node.EXPECT().SpamStatistics(r1Ctx, kp1.PublicKey()).Times(1).Return(spamStats, nil), 341 // Third request. 342 handler.node.EXPECT().SpamStatistics(r3Ctx, kp2.PublicKey()).Times(1).Return(spamStats, nil), 343 ) 344 345 // First request and third request, 346 handler.node.EXPECT().Host().Times(2).Return(nodeHost) 347 348 gomock.InOrder( 349 // First request. 350 handler.spam.EXPECT().CheckSubmission(gomock.Any(), &spamStats).Times(1).Return(nil), 351 // Third request. 352 handler.spam.EXPECT().CheckSubmission(gomock.Any(), &spamStats).Times(1).Return(nil), 353 ) 354 gomock.InOrder( 355 // First request. 356 handler.interactor.EXPECT().NotifySuccessfulTransaction(r1Ctx, r1TraceID, uint8(2), txHash, gomock.Any(), gomock.Any(), gomock.Any(), nodeHost).Times(1).Do(func(_ context.Context, _ string, _ uint8, _, _, _ string, _ time.Time, _ string) { 357 // Unblock the second and third requests, and trigger the signing. 358 close(sendSecondRequests) 359 <-waitForSecondRequestToExit 360 }), 361 // Third request. 362 handler.interactor.EXPECT().NotifySuccessfulTransaction(r3Ctx, r3TraceID, uint8(2), txHash, gomock.Any(), gomock.Any(), gomock.Any(), nodeHost).Times(1), 363 ) 364 gomock.InOrder( 365 // First request. 366 handler.interactor.EXPECT().Log(r1Ctx, r1TraceID, gomock.Any(), gomock.Any()).AnyTimes(), 367 // Third request. 368 handler.interactor.EXPECT().Log(r3Ctx, r3TraceID, gomock.Any(), gomock.Any()).AnyTimes(), 369 ) 370 371 wg := sync.WaitGroup{} 372 wg.Add(3) 373 374 go func() { 375 defer wg.Done() 376 // when 377 result, errorDetails := handler.handle(t, r1Ctx, api.ClientSendTransactionParams{ 378 PublicKey: kp1.PublicKey(), 379 SendingMode: "TYPE_SYNC", 380 Transaction: testTransaction(t), 381 }, connectedWallet) 382 383 <-waitForSecondRequestToExit 384 <-waitForThirdRequestToExit 385 386 // then 387 assert.Nil(t, errorDetails) 388 require.NotEmpty(t, result) 389 assert.NotEmpty(t, result.Transaction) 390 }() 391 392 go func() { 393 defer wg.Done() 394 395 // Closing this resume, unblock the first request. 396 defer close(waitForSecondRequestToExit) 397 398 // Ensure the first request acquire the "lock" on the public key. 399 <-sendSecondRequests 400 401 // when 402 result, errorDetails := handler.handle(t, r2Ctx, api.ClientSendTransactionParams{ 403 PublicKey: kp1.PublicKey(), 404 SendingMode: "TYPE_SYNC", 405 Transaction: testTransaction(t), 406 }, connectedWallet) 407 408 // then 409 assert.NotNil(t, errorDetails) 410 assertRequestNotPermittedError(t, errorDetails, fmt.Errorf("this public key %q is already in use, retry later", kp1.PublicKey())) 411 require.Empty(t, result) 412 }() 413 414 go func() { 415 defer wg.Done() 416 defer close(waitForThirdRequestToExit) 417 418 // Ensure the first request acquire the "lock" on the public key, and 419 // we second request calls `GetWallet()` before the third request. 420 <-sendThirdRequests 421 422 // then 423 result, errorDetails := handler.handle(t, r3Ctx, api.ClientSendTransactionParams{ 424 PublicKey: kp2.PublicKey(), 425 SendingMode: "TYPE_SYNC", 426 Transaction: testTransaction(t), 427 }, connectedWallet) 428 429 // then 430 assert.Nil(t, errorDetails) 431 require.NotEmpty(t, result) 432 assert.NotEmpty(t, result.Transaction) 433 }() 434 435 wg.Wait() 436 } 437 438 func testSendingTransactionWithoutNeededPermissionsDoesNotSendTransaction(t *testing.T) { 439 // given 440 ctx, _ := clientContextForTest() 441 hostname := vgrand.RandomStr(5) 442 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{}) 443 kp, err := wallet1.GenerateKeyPair(nil) 444 if err != nil { 445 t.Fatalf(err.Error()) 446 } 447 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 448 if err != nil { 449 t.Fatalf(err.Error()) 450 } 451 452 // setup 453 handler := newSendTransactionHandler(t) 454 455 // when 456 result, errorDetails := handler.handle(t, ctx, api.ClientSendTransactionParams{ 457 PublicKey: kp.PublicKey(), 458 SendingMode: "TYPE_SYNC", 459 Transaction: testTransaction(t), 460 }, connectedWallet) 461 462 // then 463 assertRequestNotPermittedError(t, errorDetails, api.ErrPublicKeyIsNotAllowedToBeUsed) 464 assert.Empty(t, result) 465 } 466 467 func testRefusingSendingOfTransactionDoesNotSendTransaction(t *testing.T) { 468 // given 469 ctx, traceID := clientContextForTest() 470 hostname := vgrand.RandomStr(5) 471 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 472 PublicKeys: wallet.PublicKeysPermission{ 473 Access: wallet.ReadAccess, 474 AllowedKeys: nil, 475 }, 476 }) 477 kp, err := wallet1.GenerateKeyPair(nil) 478 if err != nil { 479 t.Fatalf(err.Error()) 480 } 481 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 482 if err != nil { 483 t.Fatalf(err.Error()) 484 } 485 486 // setup 487 handler := newSendTransactionHandler(t) 488 // -- expected calls 489 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 490 handler.interactor.EXPECT().RequestTransactionReviewForSending(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(false, nil) 491 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 492 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, traceID).Times(1) 493 494 // when 495 result, errorDetails := handler.handle(t, ctx, api.ClientSendTransactionParams{ 496 PublicKey: kp.PublicKey(), 497 SendingMode: "TYPE_SYNC", 498 Transaction: testTransaction(t), 499 }, connectedWallet) 500 501 // then 502 assertUserRejectionError(t, errorDetails, api.ErrUserRejectedSendingOfTransaction) 503 assert.Empty(t, result) 504 } 505 506 func testCancellingTheReviewDoesNotSendTransaction(t *testing.T) { 507 // given 508 ctx, traceID := clientContextForTest() 509 hostname := vgrand.RandomStr(5) 510 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 511 PublicKeys: wallet.PublicKeysPermission{ 512 Access: wallet.ReadAccess, 513 AllowedKeys: nil, 514 }, 515 }) 516 kp, err := wallet1.GenerateKeyPair(nil) 517 if err != nil { 518 t.Fatalf(err.Error()) 519 } 520 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 521 if err != nil { 522 t.Fatalf(err.Error()) 523 } 524 525 // setup 526 handler := newSendTransactionHandler(t) 527 // -- expected calls 528 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 529 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, traceID).Times(1) 530 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 531 handler.interactor.EXPECT().RequestTransactionReviewForSending(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(false, api.ErrUserCloseTheConnection) 532 handler.interactor.EXPECT().NotifyError(ctx, traceID, api.ApplicationErrorType, api.ErrConnectionClosed) 533 534 // when 535 result, errorDetails := handler.handle(t, ctx, api.ClientSendTransactionParams{ 536 PublicKey: kp.PublicKey(), 537 SendingMode: "TYPE_SYNC", 538 Transaction: testTransaction(t), 539 }, connectedWallet) 540 541 // then 542 assertConnectionClosedError(t, errorDetails) 543 assert.Empty(t, result) 544 } 545 546 func testInterruptingTheRequestDoesNotSendTransaction(t *testing.T) { 547 // given 548 ctx, traceID := clientContextForTest() 549 hostname := vgrand.RandomStr(5) 550 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 551 PublicKeys: wallet.PublicKeysPermission{ 552 Access: wallet.ReadAccess, 553 AllowedKeys: nil, 554 }, 555 }) 556 kp, err := wallet1.GenerateKeyPair(nil) 557 if err != nil { 558 t.Fatalf(err.Error()) 559 } 560 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 561 if err != nil { 562 t.Fatalf(err.Error()) 563 } 564 565 // setup 566 handler := newSendTransactionHandler(t) 567 // -- expected calls 568 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 569 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, traceID).Times(1) 570 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 571 handler.interactor.EXPECT().RequestTransactionReviewForSending(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(false, api.ErrRequestInterrupted) 572 handler.interactor.EXPECT().NotifyError(ctx, traceID, api.ServerErrorType, api.ErrRequestInterrupted).Times(1) 573 574 // when 575 result, errorDetails := handler.handle(t, ctx, api.ClientSendTransactionParams{ 576 PublicKey: kp.PublicKey(), 577 SendingMode: "TYPE_SYNC", 578 Transaction: testTransaction(t), 579 }, connectedWallet) 580 581 // then 582 assertRequestInterruptionError(t, errorDetails) 583 assert.Empty(t, result) 584 } 585 586 func testGettingInternalErrorDuringReviewDoesNotSendTransaction(t *testing.T) { 587 // given 588 ctx, traceID := clientContextForTest() 589 hostname := vgrand.RandomStr(5) 590 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 591 PublicKeys: wallet.PublicKeysPermission{ 592 Access: wallet.ReadAccess, 593 AllowedKeys: nil, 594 }, 595 }) 596 kp, err := wallet1.GenerateKeyPair(nil) 597 if err != nil { 598 t.Fatalf(err.Error()) 599 } 600 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 601 if err != nil { 602 t.Fatalf(err.Error()) 603 } 604 605 // setup 606 handler := newSendTransactionHandler(t) 607 // -- expected calls 608 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 609 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, traceID).Times(1) 610 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 611 handler.interactor.EXPECT().RequestTransactionReviewForSending(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(false, assert.AnError) 612 handler.interactor.EXPECT().NotifyError(ctx, traceID, api.InternalErrorType, fmt.Errorf("requesting the transaction review failed: %w", assert.AnError)).Times(1) 613 614 // when 615 result, errorDetails := handler.handle(t, ctx, api.ClientSendTransactionParams{ 616 PublicKey: kp.PublicKey(), 617 SendingMode: "TYPE_SYNC", 618 Transaction: testTransaction(t), 619 }, connectedWallet) 620 621 // then 622 assertInternalError(t, errorDetails, api.ErrCouldNotSendTransaction) 623 assert.Empty(t, result) 624 } 625 626 func testNoHealthyNodeAvailableDoesNotSendTransaction(t *testing.T) { 627 // given 628 ctx, traceID := clientContextForTest() 629 hostname := vgrand.RandomStr(5) 630 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 631 PublicKeys: wallet.PublicKeysPermission{ 632 Access: wallet.ReadAccess, 633 AllowedKeys: nil, 634 }, 635 }) 636 kp, err := wallet1.GenerateKeyPair(nil) 637 if err != nil { 638 t.Fatalf(err.Error()) 639 } 640 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 641 if err != nil { 642 t.Fatalf(err.Error()) 643 } 644 645 // setup 646 handler := newSendTransactionHandler(t) 647 // -- expected calls 648 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 649 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, traceID).Times(1) 650 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 651 handler.interactor.EXPECT().RequestTransactionReviewForSending(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(true, nil) 652 handler.nodeSelector.EXPECT().Node(ctx, gomock.Any()).Times(1).Return(nil, assert.AnError) 653 handler.interactor.EXPECT().NotifyError(ctx, traceID, api.NetworkErrorType, fmt.Errorf("could not find a healthy node: %w", assert.AnError)).Times(1) 654 handler.interactor.EXPECT().Log(ctx, traceID, gomock.Any(), gomock.Any()).AnyTimes() 655 656 // when 657 result, errorDetails := handler.handle(t, ctx, api.ClientSendTransactionParams{ 658 PublicKey: kp.PublicKey(), 659 SendingMode: "TYPE_SYNC", 660 Transaction: testTransaction(t), 661 }, connectedWallet) 662 663 // then 664 require.NotNil(t, errorDetails) 665 assert.Equal(t, api.ErrorCodeNodeCommunicationFailed, errorDetails.Code) 666 assert.Equal(t, "Network error", errorDetails.Message) 667 assert.Equal(t, api.ErrNoHealthyNodeAvailable.Error(), errorDetails.Data) 668 assert.Empty(t, result) 669 } 670 671 func testFailingToGetSpamStatsDoesNotSendTransaction(t *testing.T) { 672 // given 673 ctx, traceID := clientContextForTest() 674 hostname := vgrand.RandomStr(5) 675 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 676 PublicKeys: wallet.PublicKeysPermission{ 677 Access: wallet.ReadAccess, 678 AllowedKeys: nil, 679 }, 680 }) 681 kp, err := wallet1.GenerateKeyPair(nil) 682 if err != nil { 683 t.Fatalf(err.Error()) 684 } 685 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 686 if err != nil { 687 t.Fatalf(err.Error()) 688 } 689 690 // setup 691 handler := newSendTransactionHandler(t) 692 // -- expected calls 693 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 694 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, traceID).Times(1) 695 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 696 handler.interactor.EXPECT().RequestTransactionReviewForSending(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(true, nil) 697 handler.nodeSelector.EXPECT().Node(ctx, gomock.Any()).Times(1).Return(handler.node, nil) 698 handler.node.EXPECT().SpamStatistics(ctx, kp.PublicKey()).Times(1).Return(types.SpamStatistics{}, assert.AnError) 699 handler.interactor.EXPECT().NotifyError(ctx, traceID, api.NetworkErrorType, fmt.Errorf("could not get the latest spam statistics for the public key from the node: %w", assert.AnError)).Times(1) 700 handler.interactor.EXPECT().Log(ctx, traceID, gomock.Any(), gomock.Any()).AnyTimes() 701 702 // when 703 result, errorDetails := handler.handle(t, ctx, api.ClientSendTransactionParams{ 704 PublicKey: kp.PublicKey(), 705 SendingMode: "TYPE_SYNC", 706 Transaction: testTransaction(t), 707 }, connectedWallet) 708 709 // then 710 require.NotNil(t, errorDetails) 711 assert.Equal(t, api.ErrorCodeNodeCommunicationFailed, errorDetails.Code) 712 assert.Equal(t, "Network error", errorDetails.Message) 713 assert.Equal(t, api.ErrCouldNotGetSpamStatistics.Error(), errorDetails.Data) 714 assert.Empty(t, result) 715 } 716 717 func testFailureWhenSendingTransactionReturnsAnError(t *testing.T) { 718 // given 719 ctx, traceID := clientContextForTest() 720 hostname := vgrand.RandomStr(5) 721 nodeHost := vgrand.RandomStr(5) 722 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 723 PublicKeys: wallet.PublicKeysPermission{ 724 Access: wallet.ReadAccess, 725 AllowedKeys: nil, 726 }, 727 }) 728 kp, err := wallet1.GenerateKeyPair(nil) 729 if err != nil { 730 t.Fatalf(err.Error()) 731 } 732 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 733 if err != nil { 734 t.Fatalf(err.Error()) 735 } 736 stats := types.SpamStatistics{ 737 ChainID: vgrand.RandomStr(5), 738 LastBlockHeight: 100, 739 } 740 741 // setup 742 handler := newSendTransactionHandler(t) 743 // -- expected calls 744 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 745 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, traceID).Times(1) 746 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 747 handler.interactor.EXPECT().RequestTransactionReviewForSending(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(true, nil) 748 handler.nodeSelector.EXPECT().Node(ctx, gomock.Any()).Times(1).Return(handler.node, nil) 749 handler.node.EXPECT().SpamStatistics(ctx, kp.PublicKey()).Times(1).Return(stats, nil) 750 handler.node.EXPECT().Host().Times(1).Return(nodeHost) 751 handler.spam.EXPECT().CheckSubmission(gomock.Any(), &stats).Times(1) 752 handler.spam.EXPECT().GenerateProofOfWork(kp.PublicKey(), &stats).Times(1).Return(&commandspb.ProofOfWork{ 753 Tid: vgrand.RandomStr(5), 754 Nonce: 12345678, 755 }, nil) 756 handler.node.EXPECT().SendTransaction(ctx, gomock.Any(), apipb.SubmitTransactionRequest_TYPE_SYNC).Times(1).Return("", assert.AnError) 757 handler.interactor.EXPECT().NotifyFailedTransaction(ctx, traceID, uint8(2), gomock.Any(), gomock.Any(), assert.AnError, gomock.Any(), nodeHost).Times(1) 758 handler.interactor.EXPECT().Log(ctx, traceID, gomock.Any(), gomock.Any()).AnyTimes() 759 760 // when 761 result, errorDetails := handler.handle(t, ctx, api.ClientSendTransactionParams{ 762 PublicKey: kp.PublicKey(), 763 SendingMode: "TYPE_SYNC", 764 Transaction: testTransaction(t), 765 }, connectedWallet) 766 767 // then 768 require.NotNil(t, errorDetails) 769 assert.Equal(t, api.ErrorCodeNodeCommunicationFailed, errorDetails.Code) 770 assert.Equal(t, "Network error", errorDetails.Message) 771 assert.Equal(t, "the transaction failed: assert.AnError general error for testing", errorDetails.Data) 772 assert.Empty(t, result) 773 } 774 775 func testFailingSpamChecksAbortsTheTransaction(t *testing.T) { 776 // given 777 ctx, traceID := clientContextForTest() 778 hostname := vgrand.RandomStr(5) 779 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 780 PublicKeys: wallet.PublicKeysPermission{ 781 Access: wallet.ReadAccess, 782 AllowedKeys: nil, 783 }, 784 }) 785 kp, err := wallet1.GenerateKeyPair(nil) 786 if err != nil { 787 t.Fatalf(err.Error()) 788 } 789 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 790 if err != nil { 791 t.Fatalf(err.Error()) 792 } 793 spamStats := types.SpamStatistics{ 794 ChainID: vgrand.RandomStr(5), 795 LastBlockHeight: 100, 796 Proposals: &types.SpamStatistic{MaxForEpoch: 1}, 797 NodeAnnouncements: &types.SpamStatistic{MaxForEpoch: 1}, 798 Delegations: &types.SpamStatistic{MaxForEpoch: 1}, 799 Transfers: &types.SpamStatistic{MaxForEpoch: 1}, 800 Votes: &types.VoteSpamStatistics{MaxForEpoch: 1}, 801 PoW: &types.PoWStatistics{ 802 PowBlockStates: []types.PoWBlockState{{}}, 803 }, 804 } 805 806 // setup 807 handler := newSendTransactionHandler(t) 808 809 // -- expected calls 810 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 811 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, traceID).Times(1) 812 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 813 handler.interactor.EXPECT().RequestTransactionReviewForSending(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(true, nil) 814 handler.nodeSelector.EXPECT().Node(ctx, gomock.Any()).Times(1).Return(handler.node, nil) 815 handler.node.EXPECT().SpamStatistics(ctx, kp.PublicKey()).Times(1).Return(spamStats, nil) 816 handler.spam.EXPECT().CheckSubmission(gomock.Any(), &spamStats).Times(1).Return(assert.AnError) 817 handler.interactor.EXPECT().NotifyError(ctx, traceID, api.ApplicationErrorType, gomock.Any()).Times(1) 818 handler.interactor.EXPECT().Log(ctx, traceID, gomock.Any(), gomock.Any()).AnyTimes() 819 820 // when 821 result, errorDetails := handler.handle(t, ctx, api.ClientSendTransactionParams{ 822 PublicKey: kp.PublicKey(), 823 SendingMode: "TYPE_SYNC", 824 Transaction: testTransaction(t), 825 }, connectedWallet) 826 827 // then 828 require.NotNil(t, errorDetails) 829 assert.Equal(t, api.ErrorCodeRequestHasBeenCancelledByApplication, errorDetails.Code) 830 assert.Equal(t, "Application error", errorDetails.Message) 831 assert.Equal(t, assert.AnError.Error(), errorDetails.Data) 832 assert.Empty(t, result) 833 } 834 835 type sendTransactionHandler struct { 836 *api.ClientSendTransaction 837 ctrl *gomock.Controller 838 interactor *mocks.MockInteractor 839 nodeSelector *nodemocks.MockSelector 840 node *nodemocks.MockNode 841 walletStore *mocks.MockWalletStore 842 spam *mocks.MockSpamHandler 843 } 844 845 func (h *sendTransactionHandler) handle(t *testing.T, ctx context.Context, params jsonrpc.Params, connectedWallet api.ConnectedWallet) (api.ClientSendTransactionResult, *jsonrpc.ErrorDetails) { 846 t.Helper() 847 848 rawResult, err := h.Handle(ctx, params, connectedWallet) 849 if rawResult != nil { 850 result, ok := rawResult.(api.ClientSendTransactionResult) 851 if !ok { 852 t.Fatal("ClientSendTransaction handler result is not a ClientSendTransactionResult") 853 } 854 return result, err 855 } 856 return api.ClientSendTransactionResult{}, err 857 } 858 859 func newSendTransactionHandler(t *testing.T) *sendTransactionHandler { 860 t.Helper() 861 862 ctrl := gomock.NewController(t) 863 nodeSelector := nodemocks.NewMockSelector(ctrl) 864 interactor := mocks.NewMockInteractor(ctrl) 865 proofOfWork := mocks.NewMockSpamHandler(ctrl) 866 walletStore := mocks.NewMockWalletStore(ctrl) 867 node := nodemocks.NewMockNode(ctrl) 868 869 requestController := api.NewRequestController( 870 api.WithMaximumAttempt(1), 871 api.WithIntervalDelayBetweenRetries(1*time.Second), 872 ) 873 874 return &sendTransactionHandler{ 875 ClientSendTransaction: api.NewClientSendTransaction(walletStore, interactor, nodeSelector, proofOfWork, requestController), 876 ctrl: ctrl, 877 nodeSelector: nodeSelector, 878 interactor: interactor, 879 node: node, 880 walletStore: walletStore, 881 spam: proofOfWork, 882 } 883 }