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