status-go/services/wallet/transfer/transaction_manager_multitransaction_test.go
saledjenic 79b1c547d1
Improvements on resolving nonce (#5658)
* chore_: unused `BuildTx` function removed from the processor interface and types that are implement it

Since the `BuildTx` function is not used anywhere, it's removed from the code.

* fix_: resolving nonce improvements

When the app sends more than a single tx from the same account on the same chain, some
chains do not return appropriate nonce (they do not consider pending txs), because of
that we place more tx with the same nonce, where all but the first one fail.

Changes in this PR keep track of nonces being used in the same sending/bridging flow, which means
for the first tx from the multi txs the app asks the chain for the nonce, and every next nonce is resolved
by incrementing the last used nonce by 1.
2024-08-12 08:07:32 -04:00

360 lines
15 KiB
Go

package transfer
import (
"context"
"encoding/json"
"math/big"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/event"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/rpc/chain"
mock_rpcclient "github.com/status-im/status-go/rpc/mock/client"
wallet_common "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/services/wallet/router/pathprocessor"
"github.com/status-im/status-go/services/wallet/router/pathprocessor/mock_pathprocessor"
"github.com/status-im/status-go/services/wallet/walletevent"
"github.com/status-im/status-go/t/helpers"
"github.com/status-im/status-go/transactions"
"github.com/status-im/status-go/transactions/mock_transactor"
"github.com/status-im/status-go/walletdatabase"
)
func deepCopy(tx *transactions.SendTxArgs) *transactions.SendTxArgs {
return &transactions.SendTxArgs{
From: tx.From,
To: tx.To,
Value: tx.Value,
Data: tx.Data,
}
}
func deepCopyTransactionBridgeWithTransferTx(tx *pathprocessor.MultipathProcessorTxArgs) *pathprocessor.MultipathProcessorTxArgs {
return &pathprocessor.MultipathProcessorTxArgs{
Name: tx.Name,
ChainID: tx.ChainID,
TransferTx: deepCopy(tx.TransferTx),
HopTx: tx.HopTx,
CbridgeTx: tx.CbridgeTx,
ERC721TransferTx: tx.ERC721TransferTx,
ERC1155TransferTx: tx.ERC1155TransferTx,
SwapTx: tx.SwapTx,
}
}
func setupTransactionManager(t *testing.T) (*TransactionManager, *mock_transactor.MockTransactorIface, *gomock.Controller) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Create a mock transactor
transactor := mock_transactor.NewMockTransactorIface(ctrl)
// Create a new instance of the TransactionManager
tm := NewTransactionManager(NewInMemMultiTransactionStorage(), nil, transactor, nil, nil, nil, nil)
return tm, transactor, ctrl
}
func setupAccount(_ *testing.T, address common.Address) *account.SelectedExtKey {
// Dummy account
return &account.SelectedExtKey{
Address: types.Address(address),
AccountKey: &types.Key{},
}
}
func setupTransactionData(_ *testing.T, transactor transactions.TransactorIface) (*MultiTransaction, []*pathprocessor.MultipathProcessorTxArgs, map[string]pathprocessor.PathProcessor, []*pathprocessor.MultipathProcessorTxArgs) {
SetMultiTransactionIDGenerator(StaticIDCounter())
// Create mock data for the test
ethTransfer := generateTestTransfer(0)
multiTransaction := GenerateTestSendMultiTransaction(ethTransfer)
// Initialize the bridges
var rpcClient *rpc.Client = nil
bridges := make(map[string]pathprocessor.PathProcessor)
transferBridge := pathprocessor.NewTransferProcessor(rpcClient, transactor)
bridges[transferBridge.Name()] = transferBridge
data := []*pathprocessor.MultipathProcessorTxArgs{
{
ChainID: 1,
Name: transferBridge.Name(),
TransferTx: &transactions.SendTxArgs{
From: types.Address(ethTransfer.From),
To: (*types.Address)(&ethTransfer.To),
Value: (*hexutil.Big)(big.NewInt(ethTransfer.Value / 3)),
Data: types.HexBytes("0x0"),
// Symbol: multiTransaction.FromAsset, // This will be set by transaction manager
// MultiTransactionID: multiTransaction.ID, // This will be set by transaction manager
},
},
{
ChainID: 420,
Name: transferBridge.Name(),
TransferTx: &transactions.SendTxArgs{
From: types.Address(ethTransfer.From),
To: (*types.Address)(&ethTransfer.To),
Value: (*hexutil.Big)(big.NewInt(ethTransfer.Value * 2 / 3)),
Data: types.HexBytes("0x0"),
// Symbol: multiTransaction.FromAsset, // This will be set by transaction manager
// MultiTransactionID: multiTransaction.ID, // This will be set by transaction manager
},
},
}
expectedData := make([]*pathprocessor.MultipathProcessorTxArgs, 0)
for _, tx := range data {
txCopy := deepCopyTransactionBridgeWithTransferTx(tx)
updateDataFromMultiTx([]*pathprocessor.MultipathProcessorTxArgs{txCopy}, &multiTransaction)
expectedData = append(expectedData, txCopy)
}
return &multiTransaction, data, bridges, expectedData
}
func setupApproveTransactionData(_ *testing.T, transactor transactions.TransactorIface) (*MultiTransaction, []*pathprocessor.MultipathProcessorTxArgs, map[string]pathprocessor.PathProcessor, []*pathprocessor.MultipathProcessorTxArgs) {
SetMultiTransactionIDGenerator(StaticIDCounter())
// Create mock data for the test
tokenTransfer := generateTestTransfer(4)
multiTransaction := GenerateTestApproveMultiTransaction(tokenTransfer)
// Initialize the bridges
var rpcClient *rpc.Client = nil
bridges := make(map[string]pathprocessor.PathProcessor)
transferBridge := pathprocessor.NewTransferProcessor(rpcClient, transactor)
bridges[transferBridge.Name()] = transferBridge
data := []*pathprocessor.MultipathProcessorTxArgs{
{
//ChainID: 1, // This will be set by transaction manager
Name: transferBridge.Name(),
TransferTx: &transactions.SendTxArgs{
From: types.Address(tokenTransfer.From),
To: (*types.Address)(&tokenTransfer.To),
Value: (*hexutil.Big)(big.NewInt(tokenTransfer.Value)),
Data: types.HexBytes("0x0"),
// Symbol: multiTransaction.FromAsset, // This will be set by transaction manager
// MultiTransactionID: multiTransaction.ID, // This will be set by transaction manager
},
},
}
expectedData := make([]*pathprocessor.MultipathProcessorTxArgs, 0)
for _, tx := range data {
txCopy := deepCopyTransactionBridgeWithTransferTx(tx)
updateDataFromMultiTx([]*pathprocessor.MultipathProcessorTxArgs{txCopy}, &multiTransaction)
expectedData = append(expectedData, txCopy)
}
return &multiTransaction, data, bridges, expectedData
}
func TestSendTransactionsETHSuccess(t *testing.T) {
tm, transactor, _ := setupTransactionManager(t)
account := setupAccount(t, common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"))
multiTransaction, data, bridges, expectedData := setupTransactionData(t, transactor)
// Verify that the SendTransactionWithChainID method is called for each transaction with proper arguments
// Return values are not checked, because they must be checked in Transactor tests
for _, tx := range expectedData {
transactor.EXPECT().SendTransactionWithChainID(tx.ChainID, *(tx.TransferTx), int64(-1), account).Return(types.Hash{}, uint64(0), nil)
}
// Call the SendTransactions method
_, err := tm.SendTransactions(context.Background(), multiTransaction, data, bridges, account)
require.NoError(t, err)
}
func TestSendTransactionsApproveSuccess(t *testing.T) {
tm, transactor, _ := setupTransactionManager(t)
account := setupAccount(t, common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"))
multiTransaction, data, bridges, expectedData := setupApproveTransactionData(t, transactor)
// Verify that the SendTransactionWithChainID method is called for each transaction with proper arguments
// Return values are not checked, because they must be checked in Transactor tests
for _, tx := range expectedData {
transactor.EXPECT().SendTransactionWithChainID(tx.ChainID, *(tx.TransferTx), int64(-1), account).Return(types.Hash{}, uint64(0), nil)
}
// Call the SendTransactions method
_, err := tm.SendTransactions(context.Background(), multiTransaction, data, bridges, account)
require.NoError(t, err)
}
func TestSendTransactionsETHFailOnBridge(t *testing.T) {
tm, transactor, ctrl := setupTransactionManager(t)
account := setupAccount(t, common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"))
multiTransaction, data, _, _ := setupTransactionData(t, transactor)
// Initialize the bridges
bridges := make(map[string]pathprocessor.PathProcessor)
transferBridge := mock_pathprocessor.NewMockPathProcessor(ctrl)
// Set bridge name for the mock to the one used in data
transferBridge.EXPECT().Name().Return(data[0].Name).AnyTimes()
bridges[transferBridge.Name()] = transferBridge
expectedErr := transactions.ErrInvalidTxSender // Any error to verify
// In case of bridge error, verify that the error is returned
transferBridge.EXPECT().Send(gomock.Any(), int64(-1), gomock.Any()).Return(types.Hash{}, uint64(0), transactions.ErrInvalidTxSender)
// Call the SendTransactions method
_, err := tm.SendTransactions(context.Background(), multiTransaction, data, bridges, account)
require.ErrorIs(t, expectedErr, err)
}
func TestSendTransactionsETHFailOnTransactor(t *testing.T) {
tm, transactor, _ := setupTransactionManager(t)
account := setupAccount(t, common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"))
multiTransaction, data, bridges, expectedData := setupTransactionData(t, transactor)
// Verify that the SendTransactionWithChainID method is called for each transaction with proper arguments
// Return values are not checked, because they must be checked in Transactor tests. Only error propagation matters here
expectedErr := transactions.ErrInvalidTxSender // Any error to verify
transactor.EXPECT().SendTransactionWithChainID(expectedData[0].ChainID, *(expectedData[0].TransferTx), int64(-1), account).Return(types.Hash{}, uint64(0), nil)
transactor.EXPECT().SendTransactionWithChainID(expectedData[1].ChainID, *(expectedData[1].TransferTx), int64(-1), account).Return(types.Hash{}, uint64(0), expectedErr)
// Call the SendTransactions method
_, err := tm.SendTransactions(context.Background(), multiTransaction, data, bridges, account)
require.ErrorIs(t, expectedErr, err)
}
func TestWatchTransaction(t *testing.T) {
tm, _, _ := setupTransactionManager(t)
chainID := uint64(777) // GeneratePendingTransaction uses this chainID
pendingTxTimeout = 2 * time.Millisecond
walletDB, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{})
require.NoError(t, err)
chainClient := transactions.NewMockChainClient()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
rpcClient := mock_rpcclient.NewMockClientInterface(ctrl)
rpcClient.EXPECT().AbstractEthClient(wallet_common.ChainID(chainID)).DoAndReturn(func(chainID wallet_common.ChainID) (chain.BatchCallClient, error) {
return chainClient.AbstractEthClient(chainID)
}).AnyTimes()
eventFeed := &event.Feed{}
// For now, pending tracker is not interface, so we have to use a real one
tm.pendingTracker = transactions.NewPendingTxTracker(walletDB, rpcClient, nil, eventFeed, pendingTxTimeout)
tm.eventFeed = eventFeed
// Create a context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 2*pendingTxTimeout)
defer cancel()
// Insert a pending transaction
txs := transactions.MockTestTransactions(t, chainClient, []transactions.TestTxSummary{{}})
err = tm.pendingTracker.StoreAndTrackPendingTx(&txs[0]) // We dont need to track it, but no other way to insert it
require.NoError(t, err)
txEventPayload := transactions.StatusChangedPayload{
TxIdentity: transactions.TxIdentity{
Hash: txs[0].Hash,
ChainID: wallet_common.ChainID(chainID),
},
Status: transactions.Pending,
}
jsonPayload, err := json.Marshal(txEventPayload)
require.NoError(t, err)
go func() {
time.Sleep(pendingTxTimeout / 2)
eventFeed.Send(walletevent.Event{
Type: transactions.EventPendingTransactionStatusChanged,
Message: string(jsonPayload),
})
}()
// Call the WatchTransaction method
err = tm.WatchTransaction(ctx, chainID, txs[0].Hash)
require.NoError(t, err)
}
func TestWatchTransaction_Timeout(t *testing.T) {
tm, _, _ := setupTransactionManager(t)
chainID := uint64(777) // GeneratePendingTransaction uses this chainID
transactionHash := common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
pendingTxTimeout = 2 * time.Millisecond
walletDB, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{})
require.NoError(t, err)
chainClient := transactions.NewMockChainClient()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
rpcClient := mock_rpcclient.NewMockClientInterface(gomock.NewController(t))
rpcClient.EXPECT().AbstractEthClient(wallet_common.ChainID(chainID)).DoAndReturn(func(chainID wallet_common.ChainID) (chain.BatchCallClient, error) {
return chainClient.AbstractEthClient(chainID)
}).AnyTimes()
eventFeed := &event.Feed{}
// For now, pending tracker is not interface, so we have to use a real one
tm.pendingTracker = transactions.NewPendingTxTracker(walletDB, rpcClient, nil, eventFeed, pendingTxTimeout)
tm.eventFeed = eventFeed
// Create a context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Microsecond)
defer cancel()
// Insert a pending transaction
txs := transactions.MockTestTransactions(t, chainClient, []transactions.TestTxSummary{{}})
err = tm.pendingTracker.StoreAndTrackPendingTx(&txs[0]) // We dont need to track it, but no other way to insert it
require.NoError(t, err)
// Call the WatchTransaction method
err = tm.WatchTransaction(ctx, chainID, transactionHash)
require.ErrorIs(t, err, ErrWatchPendingTxTimeout)
}
func TestCreateMultiTransactionFromCommand(t *testing.T) {
tm, _, _ := setupTransactionManager(t)
var command *MultiTransactionCommand
// Test types that should get chainID from the data
mtTypes := []MultiTransactionType{MultiTransactionSend, MultiTransactionApprove, MultiTransactionSwap, MultiTransactionType(7)}
for _, mtType := range mtTypes {
fromAmount := hexutil.Big(*big.NewInt(1000000000000000000))
toAmount := hexutil.Big(*big.NewInt(123))
command = &MultiTransactionCommand{
Type: mtType,
FromAddress: common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"),
ToAddress: common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12"),
FromAsset: "DAI",
ToAsset: "USDT",
FromAmount: &fromAmount,
ToAmount: &toAmount,
}
data := make([]*pathprocessor.MultipathProcessorTxArgs, 0)
data = append(data, &pathprocessor.MultipathProcessorTxArgs{
ChainID: 1,
})
multiTransaction, err := tm.CreateMultiTransactionFromCommand(command, data)
if mtType > MultiTransactionApprove {
// Unsupported type
require.Error(t, err)
break
}
require.NoError(t, err)
require.NotNil(t, multiTransaction)
require.Equal(t, command.FromAddress, multiTransaction.FromAddress)
require.Equal(t, command.ToAddress, multiTransaction.ToAddress)
require.Equal(t, command.FromAsset, multiTransaction.FromAsset)
require.Equal(t, command.ToAsset, multiTransaction.ToAsset)
require.Equal(t, command.FromAmount, multiTransaction.FromAmount)
require.Equal(t, command.ToAmount, multiTransaction.ToAmount)
require.Equal(t, command.Type, multiTransaction.Type)
require.Equal(t, data[0].ChainID, multiTransaction.FromNetworkID)
}
}