code.vegaprotocol.io/vega@v0.79.0/wallet/api/client_send_transaction_test.go (about)

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