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)(ðTransfer.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)(ðTransfer.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 }