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  }