github.com/status-im/status-go@v1.1.0/services/wallet/transfer/transaction_manager_multitransaction_test.go (about)

     1  package transfer
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"math/big"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/golang/mock/gomock"
    11  	"github.com/stretchr/testify/require"
    12  
    13  	"github.com/ethereum/go-ethereum/common"
    14  	"github.com/ethereum/go-ethereum/common/hexutil"
    15  	"github.com/ethereum/go-ethereum/event"
    16  	"github.com/status-im/status-go/account"
    17  	"github.com/status-im/status-go/eth-node/types"
    18  	"github.com/status-im/status-go/rpc"
    19  	"github.com/status-im/status-go/rpc/chain"
    20  	mock_rpcclient "github.com/status-im/status-go/rpc/mock/client"
    21  	wallet_common "github.com/status-im/status-go/services/wallet/common"
    22  	"github.com/status-im/status-go/services/wallet/router/pathprocessor"
    23  	"github.com/status-im/status-go/services/wallet/router/pathprocessor/mock_pathprocessor"
    24  	"github.com/status-im/status-go/services/wallet/walletevent"
    25  	"github.com/status-im/status-go/t/helpers"
    26  	"github.com/status-im/status-go/transactions"
    27  	"github.com/status-im/status-go/transactions/mock_transactor"
    28  	"github.com/status-im/status-go/walletdatabase"
    29  )
    30  
    31  func deepCopy(tx *transactions.SendTxArgs) *transactions.SendTxArgs {
    32  	return &transactions.SendTxArgs{
    33  		From:  tx.From,
    34  		To:    tx.To,
    35  		Value: tx.Value,
    36  		Data:  tx.Data,
    37  	}
    38  }
    39  
    40  func deepCopyTransactionBridgeWithTransferTx(tx *pathprocessor.MultipathProcessorTxArgs) *pathprocessor.MultipathProcessorTxArgs {
    41  	return &pathprocessor.MultipathProcessorTxArgs{
    42  		Name:              tx.Name,
    43  		ChainID:           tx.ChainID,
    44  		TransferTx:        deepCopy(tx.TransferTx),
    45  		HopTx:             tx.HopTx,
    46  		CbridgeTx:         tx.CbridgeTx,
    47  		ERC721TransferTx:  tx.ERC721TransferTx,
    48  		ERC1155TransferTx: tx.ERC1155TransferTx,
    49  		SwapTx:            tx.SwapTx,
    50  	}
    51  }
    52  
    53  func setupTransactionManager(t *testing.T) (*TransactionManager, *mock_transactor.MockTransactorIface, *gomock.Controller) {
    54  	ctrl := gomock.NewController(t)
    55  	defer ctrl.Finish()
    56  
    57  	// Create a mock transactor
    58  	transactor := mock_transactor.NewMockTransactorIface(ctrl)
    59  	// Create a new instance of the TransactionManager
    60  	tm := NewTransactionManager(NewInMemMultiTransactionStorage(), nil, transactor, nil, nil, nil, nil)
    61  
    62  	return tm, transactor, ctrl
    63  }
    64  
    65  func setupAccount(_ *testing.T, address common.Address) *account.SelectedExtKey {
    66  	// Dummy account
    67  	return &account.SelectedExtKey{
    68  		Address:    types.Address(address),
    69  		AccountKey: &types.Key{},
    70  	}
    71  }
    72  
    73  func setupTransactionData(_ *testing.T, transactor transactions.TransactorIface) (*MultiTransaction, []*pathprocessor.MultipathProcessorTxArgs, map[string]pathprocessor.PathProcessor, []*pathprocessor.MultipathProcessorTxArgs) {
    74  	SetMultiTransactionIDGenerator(StaticIDCounter())
    75  
    76  	// Create mock data for the test
    77  	ethTransfer := generateTestTransfer(0)
    78  	multiTransaction := GenerateTestSendMultiTransaction(ethTransfer)
    79  
    80  	// Initialize the bridges
    81  	var rpcClient *rpc.Client = nil
    82  	bridges := make(map[string]pathprocessor.PathProcessor)
    83  	transferBridge := pathprocessor.NewTransferProcessor(rpcClient, transactor)
    84  	bridges[transferBridge.Name()] = transferBridge
    85  
    86  	data := []*pathprocessor.MultipathProcessorTxArgs{
    87  		{
    88  			ChainID: 1,
    89  			Name:    transferBridge.Name(),
    90  			TransferTx: &transactions.SendTxArgs{
    91  				From:  types.Address(ethTransfer.From),
    92  				To:    (*types.Address)(&ethTransfer.To),
    93  				Value: (*hexutil.Big)(big.NewInt(ethTransfer.Value / 3)),
    94  				Data:  types.HexBytes("0x0"),
    95  				// Symbol: multiTransaction.FromAsset, // This will be set by transaction manager
    96  				// MultiTransactionID: multiTransaction.ID, // This will be set by transaction manager
    97  			},
    98  		},
    99  		{
   100  			ChainID: 420,
   101  			Name:    transferBridge.Name(),
   102  			TransferTx: &transactions.SendTxArgs{
   103  				From:  types.Address(ethTransfer.From),
   104  				To:    (*types.Address)(&ethTransfer.To),
   105  				Value: (*hexutil.Big)(big.NewInt(ethTransfer.Value * 2 / 3)),
   106  				Data:  types.HexBytes("0x0"),
   107  				// Symbol: multiTransaction.FromAsset, // This will be set by transaction manager
   108  				// MultiTransactionID: multiTransaction.ID, // This will be set by transaction manager
   109  			},
   110  		},
   111  	}
   112  
   113  	expectedData := make([]*pathprocessor.MultipathProcessorTxArgs, 0)
   114  	for _, tx := range data {
   115  		txCopy := deepCopyTransactionBridgeWithTransferTx(tx)
   116  		updateDataFromMultiTx([]*pathprocessor.MultipathProcessorTxArgs{txCopy}, &multiTransaction)
   117  		expectedData = append(expectedData, txCopy)
   118  	}
   119  
   120  	return &multiTransaction, data, bridges, expectedData
   121  }
   122  
   123  func setupApproveTransactionData(_ *testing.T, transactor transactions.TransactorIface) (*MultiTransaction, []*pathprocessor.MultipathProcessorTxArgs, map[string]pathprocessor.PathProcessor, []*pathprocessor.MultipathProcessorTxArgs) {
   124  	SetMultiTransactionIDGenerator(StaticIDCounter())
   125  
   126  	// Create mock data for the test
   127  	tokenTransfer := generateTestTransfer(4)
   128  	multiTransaction := GenerateTestApproveMultiTransaction(tokenTransfer)
   129  
   130  	// Initialize the bridges
   131  	var rpcClient *rpc.Client = nil
   132  	bridges := make(map[string]pathprocessor.PathProcessor)
   133  	transferBridge := pathprocessor.NewTransferProcessor(rpcClient, transactor)
   134  	bridges[transferBridge.Name()] = transferBridge
   135  
   136  	data := []*pathprocessor.MultipathProcessorTxArgs{
   137  		{
   138  			//ChainID: 1, // This will be set by transaction manager
   139  			Name: transferBridge.Name(),
   140  			TransferTx: &transactions.SendTxArgs{
   141  				From:  types.Address(tokenTransfer.From),
   142  				To:    (*types.Address)(&tokenTransfer.To),
   143  				Value: (*hexutil.Big)(big.NewInt(tokenTransfer.Value)),
   144  				Data:  types.HexBytes("0x0"),
   145  				// Symbol: multiTransaction.FromAsset, // This will be set by transaction manager
   146  				// MultiTransactionID: multiTransaction.ID, // This will be set by transaction manager
   147  			},
   148  		},
   149  	}
   150  
   151  	expectedData := make([]*pathprocessor.MultipathProcessorTxArgs, 0)
   152  	for _, tx := range data {
   153  		txCopy := deepCopyTransactionBridgeWithTransferTx(tx)
   154  		updateDataFromMultiTx([]*pathprocessor.MultipathProcessorTxArgs{txCopy}, &multiTransaction)
   155  		expectedData = append(expectedData, txCopy)
   156  	}
   157  
   158  	return &multiTransaction, data, bridges, expectedData
   159  }
   160  
   161  func TestSendTransactionsETHSuccess(t *testing.T) {
   162  	tm, transactor, _ := setupTransactionManager(t)
   163  	account := setupAccount(t, common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"))
   164  	multiTransaction, data, bridges, expectedData := setupTransactionData(t, transactor)
   165  
   166  	// Verify that the SendTransactionWithChainID method is called for each transaction with proper arguments
   167  	// Return values are not checked, because they must be checked in Transactor tests
   168  	for _, tx := range expectedData {
   169  		transactor.EXPECT().SendTransactionWithChainID(tx.ChainID, *(tx.TransferTx), int64(-1), account).Return(types.Hash{}, uint64(0), nil)
   170  	}
   171  
   172  	// Call the SendTransactions method
   173  	_, err := tm.SendTransactions(context.Background(), multiTransaction, data, bridges, account)
   174  	require.NoError(t, err)
   175  }
   176  
   177  func TestSendTransactionsApproveSuccess(t *testing.T) {
   178  	tm, transactor, _ := setupTransactionManager(t)
   179  	account := setupAccount(t, common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"))
   180  	multiTransaction, data, bridges, expectedData := setupApproveTransactionData(t, transactor)
   181  
   182  	// Verify that the SendTransactionWithChainID method is called for each transaction with proper arguments
   183  	// Return values are not checked, because they must be checked in Transactor tests
   184  	for _, tx := range expectedData {
   185  		transactor.EXPECT().SendTransactionWithChainID(tx.ChainID, *(tx.TransferTx), int64(-1), account).Return(types.Hash{}, uint64(0), nil)
   186  	}
   187  
   188  	// Call the SendTransactions method
   189  	_, err := tm.SendTransactions(context.Background(), multiTransaction, data, bridges, account)
   190  	require.NoError(t, err)
   191  }
   192  
   193  func TestSendTransactionsETHFailOnBridge(t *testing.T) {
   194  	tm, transactor, ctrl := setupTransactionManager(t)
   195  	account := setupAccount(t, common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"))
   196  	multiTransaction, data, _, _ := setupTransactionData(t, transactor)
   197  
   198  	// Initialize the bridges
   199  	bridges := make(map[string]pathprocessor.PathProcessor)
   200  	transferBridge := mock_pathprocessor.NewMockPathProcessor(ctrl)
   201  
   202  	// Set bridge name for the mock to the one used in data
   203  	transferBridge.EXPECT().Name().Return(data[0].Name).AnyTimes()
   204  	bridges[transferBridge.Name()] = transferBridge
   205  
   206  	expectedErr := transactions.ErrInvalidTxSender // Any error to verify
   207  	// In case of bridge error, verify that the error is returned
   208  	transferBridge.EXPECT().Send(gomock.Any(), int64(-1), gomock.Any()).Return(types.Hash{}, uint64(0), transactions.ErrInvalidTxSender)
   209  
   210  	// Call the SendTransactions method
   211  	_, err := tm.SendTransactions(context.Background(), multiTransaction, data, bridges, account)
   212  	require.ErrorIs(t, expectedErr, err)
   213  }
   214  
   215  func TestSendTransactionsETHFailOnTransactor(t *testing.T) {
   216  	tm, transactor, _ := setupTransactionManager(t)
   217  	account := setupAccount(t, common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"))
   218  	multiTransaction, data, bridges, expectedData := setupTransactionData(t, transactor)
   219  
   220  	// Verify that the SendTransactionWithChainID method is called for each transaction with proper arguments
   221  	// Return values are not checked, because they must be checked in Transactor tests. Only error propagation matters here
   222  	expectedErr := transactions.ErrInvalidTxSender // Any error to verify
   223  	transactor.EXPECT().SendTransactionWithChainID(expectedData[0].ChainID, *(expectedData[0].TransferTx), int64(-1), account).Return(types.Hash{}, uint64(0), nil)
   224  	transactor.EXPECT().SendTransactionWithChainID(expectedData[1].ChainID, *(expectedData[1].TransferTx), int64(-1), account).Return(types.Hash{}, uint64(0), expectedErr)
   225  
   226  	// Call the SendTransactions method
   227  	_, err := tm.SendTransactions(context.Background(), multiTransaction, data, bridges, account)
   228  	require.ErrorIs(t, expectedErr, err)
   229  }
   230  
   231  func TestWatchTransaction(t *testing.T) {
   232  	tm, _, _ := setupTransactionManager(t)
   233  	chainID := uint64(777) // GeneratePendingTransaction uses this chainID
   234  	pendingTxTimeout = 2 * time.Millisecond
   235  
   236  	walletDB, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{})
   237  	require.NoError(t, err)
   238  	chainClient := transactions.NewMockChainClient()
   239  	ctrl := gomock.NewController(t)
   240  	defer ctrl.Finish()
   241  	rpcClient := mock_rpcclient.NewMockClientInterface(ctrl)
   242  	rpcClient.EXPECT().AbstractEthClient(wallet_common.ChainID(chainID)).DoAndReturn(func(chainID wallet_common.ChainID) (chain.BatchCallClient, error) {
   243  		return chainClient.AbstractEthClient(chainID)
   244  	}).AnyTimes()
   245  	eventFeed := &event.Feed{}
   246  	// For now, pending tracker is not interface, so we have to use a real one
   247  	tm.pendingTracker = transactions.NewPendingTxTracker(walletDB, rpcClient, nil, eventFeed, pendingTxTimeout)
   248  	tm.eventFeed = eventFeed
   249  
   250  	// Create a context with timeout
   251  	ctx, cancel := context.WithTimeout(context.Background(), 2*pendingTxTimeout)
   252  	defer cancel()
   253  
   254  	// Insert a pending transaction
   255  	txs := transactions.MockTestTransactions(t, chainClient, []transactions.TestTxSummary{{}})
   256  	err = tm.pendingTracker.StoreAndTrackPendingTx(&txs[0]) // We dont need to track it, but no other way to insert it
   257  	require.NoError(t, err)
   258  
   259  	txEventPayload := transactions.StatusChangedPayload{
   260  		TxIdentity: transactions.TxIdentity{
   261  			Hash:    txs[0].Hash,
   262  			ChainID: wallet_common.ChainID(chainID),
   263  		},
   264  		Status: transactions.Pending,
   265  	}
   266  	jsonPayload, err := json.Marshal(txEventPayload)
   267  	require.NoError(t, err)
   268  
   269  	go func() {
   270  		time.Sleep(pendingTxTimeout / 2)
   271  		eventFeed.Send(walletevent.Event{
   272  			Type:    transactions.EventPendingTransactionStatusChanged,
   273  			Message: string(jsonPayload),
   274  		})
   275  	}()
   276  
   277  	// Call the WatchTransaction method
   278  	err = tm.WatchTransaction(ctx, chainID, txs[0].Hash)
   279  	require.NoError(t, err)
   280  }
   281  
   282  func TestWatchTransaction_Timeout(t *testing.T) {
   283  	tm, _, _ := setupTransactionManager(t)
   284  	chainID := uint64(777) // GeneratePendingTransaction uses this chainID
   285  	transactionHash := common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
   286  	pendingTxTimeout = 2 * time.Millisecond
   287  
   288  	walletDB, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{})
   289  	require.NoError(t, err)
   290  	chainClient := transactions.NewMockChainClient()
   291  	ctrl := gomock.NewController(t)
   292  	defer ctrl.Finish()
   293  	rpcClient := mock_rpcclient.NewMockClientInterface(gomock.NewController(t))
   294  	rpcClient.EXPECT().AbstractEthClient(wallet_common.ChainID(chainID)).DoAndReturn(func(chainID wallet_common.ChainID) (chain.BatchCallClient, error) {
   295  		return chainClient.AbstractEthClient(chainID)
   296  	}).AnyTimes()
   297  	eventFeed := &event.Feed{}
   298  	// For now, pending tracker is not interface, so we have to use a real one
   299  	tm.pendingTracker = transactions.NewPendingTxTracker(walletDB, rpcClient, nil, eventFeed, pendingTxTimeout)
   300  	tm.eventFeed = eventFeed
   301  
   302  	// Create a context with timeout
   303  	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Microsecond)
   304  	defer cancel()
   305  
   306  	// Insert a pending transaction
   307  	txs := transactions.MockTestTransactions(t, chainClient, []transactions.TestTxSummary{{}})
   308  	err = tm.pendingTracker.StoreAndTrackPendingTx(&txs[0]) // We dont need to track it, but no other way to insert it
   309  	require.NoError(t, err)
   310  
   311  	// Call the WatchTransaction method
   312  	err = tm.WatchTransaction(ctx, chainID, transactionHash)
   313  	require.ErrorIs(t, err, ErrWatchPendingTxTimeout)
   314  }
   315  
   316  func TestCreateMultiTransactionFromCommand(t *testing.T) {
   317  	tm, _, _ := setupTransactionManager(t)
   318  
   319  	var command *MultiTransactionCommand
   320  
   321  	// Test types that should get chainID from the data
   322  	mtTypes := []MultiTransactionType{MultiTransactionSend, MultiTransactionApprove, MultiTransactionSwap, MultiTransactionBridge, MultiTransactionType(7)}
   323  
   324  	for _, mtType := range mtTypes {
   325  		fromAmount := hexutil.Big(*big.NewInt(1000000000000000000))
   326  		toAmount := hexutil.Big(*big.NewInt(123))
   327  		command = &MultiTransactionCommand{
   328  			Type:        mtType,
   329  			FromAddress: common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"),
   330  			ToAddress:   common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12"),
   331  			FromAsset:   "DAI",
   332  			ToAsset:     "USDT",
   333  			FromAmount:  &fromAmount,
   334  			ToAmount:    &toAmount,
   335  		}
   336  
   337  		data := make([]*pathprocessor.MultipathProcessorTxArgs, 0)
   338  		data = append(data, &pathprocessor.MultipathProcessorTxArgs{
   339  			ChainID: 1,
   340  		})
   341  
   342  		if mtType == MultiTransactionBridge {
   343  			data[0].HopTx = &pathprocessor.HopBridgeTxArgs{
   344  				ChainID:   1,
   345  				ChainIDTo: 2,
   346  			}
   347  		}
   348  
   349  		multiTransaction, err := tm.CreateMultiTransactionFromCommand(command, data)
   350  		if mtType > MultiTransactionApprove {
   351  			// Unsupported type
   352  			require.Error(t, err)
   353  			break
   354  		}
   355  		require.NoError(t, err)
   356  		require.NotNil(t, multiTransaction)
   357  		require.Equal(t, command.FromAddress, multiTransaction.FromAddress)
   358  		require.Equal(t, command.ToAddress, multiTransaction.ToAddress)
   359  		require.Equal(t, command.FromAsset, multiTransaction.FromAsset)
   360  		require.Equal(t, command.ToAsset, multiTransaction.ToAsset)
   361  		require.Equal(t, command.FromAmount, multiTransaction.FromAmount)
   362  		require.Equal(t, command.ToAmount, multiTransaction.ToAmount)
   363  		require.Equal(t, command.Type, multiTransaction.Type)
   364  		require.Equal(t, data[0].ChainID, multiTransaction.FromNetworkID)
   365  	}
   366  }