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