diff --git a/test/status-go/integration/helpers/helpers.go b/test/status-go/integration/helpers/helpers.go index df3cdb1e60..96cd465f00 100644 --- a/test/status-go/integration/helpers/helpers.go +++ b/test/status-go/integration/helpers/helpers.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" statusgo "github.com/status-im/status-go/mobile" "github.com/status-im/status-go/multiaccounts" @@ -107,18 +108,31 @@ func WaitForEvent(eventQueue chan GoEvent, eventName StatusGoEventName, timeout } } -// WaitForWalletEvents returns payloads corresponding to the given eventNames in the order they are received for duplicate events -func WaitForWalletEvents[T any](eventQueue chan GoEvent, eventNamesOrig []walletevent.EventType, timeout time.Duration) (payloads []*T, err error) { - var event *GoEvent +func WaitForWalletEvents(eventQueue chan GoEvent, eventNames []walletevent.EventType, timeout time.Duration, condition func(walletEvent *walletevent.Event) bool) (walletEvents []*walletevent.Event, err error) { + return WaitForWalletEventsWithOptionals(eventQueue, eventNames, timeout, condition, nil) +} - payloads = make([]*T, len(eventNamesOrig)) - processed := make([]bool, len(eventNamesOrig)) - processedCount := 0 +// WaitForWalletEvents waits for the given events to be received on the eventQueue. +// It returns the wallet events in the order they are received. +func WaitForWalletEventsWithOptionals(eventQueue chan GoEvent, eventNames []walletevent.EventType, timeout time.Duration, condition func(walletEvent *walletevent.Event) bool, optionalEventNames []walletevent.EventType) (walletEvents []*walletevent.Event, err error) { + if len(eventNames) == 0 { + return nil, errors.New("no event names provided") + } + startTime := time.Now() + expected := make([]walletevent.EventType, len(eventNames)) + copy(expected, eventNames) + walletEvents = make([]*walletevent.Event, 0, len(eventNames)) + +infiniteLoop: for { - event, err = WaitForEvent(eventQueue, WalletEvent, timeout) + toWait := timeout - time.Since(startTime) + if toWait <= 0 { + return nil, fmt.Errorf("timeout waiting for events %+v", expected) + } + event, err := WaitForEvent(eventQueue, WalletEvent, toWait) if err != nil { - return nil, err + return nil, fmt.Errorf("error waiting for events %+v: %w", expected, err) } walletEvent, ok := event.Payload.(walletevent.Event) @@ -126,40 +140,115 @@ func WaitForWalletEvents[T any](eventQueue chan GoEvent, eventNamesOrig []wallet return nil, errors.New("event payload is not a wallet event") } - var newPayload T - foundIndex := -1 - for i, eventName := range eventNamesOrig { - if walletEvent.Type == eventName && !processed[i] { - foundIndex = i - processed[i] = true - processedCount += 1 - break + for i, event := range expected { + if walletEvent.Type == event && (condition == nil || condition(&walletEvent)) { + walletEvents = append(walletEvents, &walletEvent) + if len(expected) == 1 { + return walletEvents, nil + } + // Remove found event from the list of expected events + expected = append(expected[:i], expected[i+1:]...) + continue infiniteLoop } } - - if foundIndex != -1 { - if walletEvent.Message != "" { - err = json.Unmarshal([]byte(walletEvent.Message), &newPayload) - if err != nil { - return nil, err - } - payloads[foundIndex] = &newPayload - } else { - payloads[foundIndex] = nil - } - if processedCount == len(eventNamesOrig) { - return payloads, nil + for _, event := range optionalEventNames { + if walletEvent.Type == event && condition != nil { + _ = condition(&walletEvent) } } } } -func WaitForWalletEvent[T any](eventQueue chan GoEvent, eventName walletevent.EventType, timeout time.Duration) (payload *T, err error) { - res, err := WaitForWalletEvents[T](eventQueue, []walletevent.EventType{eventName}, timeout) +type payloadRes struct { + eventName walletevent.EventType + data []byte +} + +// WaitForWalletEventsGetPayloads returns payloads corresponding to the given eventNames in the order they are received for duplicate events +func WaitForWalletEventsGetPayloads(eventQueue chan GoEvent, eventNames []walletevent.EventType, timeoutEach time.Duration) (payloads []payloadRes, err error) { + walletEvents, err := WaitForWalletEvents(eventQueue, eventNames, timeoutEach, nil) if err != nil { return nil, err } - return res[0], nil + + payloads = make([]payloadRes, len(walletEvents)) + for i, event := range walletEvents { + payloads[i] = payloadRes{ + eventName: event.Type, + } + if event.Message != "" { + payloads[i].data = []byte(event.Message) + } + } + return payloads, nil +} + +type payloadMapRes struct { + EventName walletevent.EventType + JsonData map[string]interface{} +} + +// WaitForWalletEventsGetMap returns parsed JSON payloads; @see WaitForWalletEventsGetPayloads +func WaitForWalletEventsGetMap(eventQueue chan GoEvent, eventNames []walletevent.EventType, timeout time.Duration) (payloads []payloadMapRes, err error) { + bytePayloads, err := WaitForWalletEventsGetPayloads(eventQueue, eventNames, timeout) + if err != nil { + return nil, err + } + payloads = make([]payloadMapRes, len(bytePayloads)) + for i, payload := range bytePayloads { + var mapPayload map[string]interface{} + if payload.data != nil { + mapPayload = make(map[string]interface{}) + err = json.Unmarshal(payload.data, &mapPayload) + if err != nil { + return nil, err + } + } + payloads[i] = payloadMapRes{ + EventName: payload.eventName, + JsonData: mapPayload, + } + } + return payloads, nil +} + +func WaitForWalletEventGetPayload[T any](eventQueue chan GoEvent, eventName walletevent.EventType, timeout time.Duration) (payload *T, err error) { + res, err := WaitForWalletEventsGetPayloads(eventQueue, []walletevent.EventType{eventName}, timeout) + if err != nil { + return nil, err + } + if res[0].data == nil { + return nil, nil + } + + newPayload := new(T) + err = json.Unmarshal(res[0].data, newPayload) + if err != nil { + return nil, err + } + return newPayload, nil +} + +// WaitForTxDownloaderToFinishForAccountsCondition returns a state-full condition function that records every account that has been seen with the events until the entire list is seen +func WaitForTxDownloaderToFinishForAccountsCondition(t *testing.T, accounts []common.Address) func(walletEvent *walletevent.Event) bool { + accs := make([]common.Address, len(accounts)) + copy(accs, accounts) + + return func(walletEvent *walletevent.Event) bool { + eventAccountsLoop: + for _, acc := range walletEvent.Accounts { + for i, a := range accs { + if acc == a { + if len(accs) == 1 { + return true + } + accs = append(accs[:i], accs[i+1:]...) + continue eventAccountsLoop + } + } + } + return false + } } func loginToAccount(hashedPassword, userFolder, nodeConfigJson string) error { @@ -251,32 +340,49 @@ func CallPrivateMethodWithTimeout(method string, params []interface{}, timeout t } didTimeout := false - done := make(chan bool) - var responseJson string + done := make(chan string) go func() { - responseJson = statusgo.CallPrivateRPC(string(msgJson)) + responseJson := statusgo.CallPrivateRPC(string(msgJson)) if didTimeout { log.Warn("Call to CallPrivateRPC returned after timeout", "payload", string(msgJson)) return } - done <- true + done <- responseJson }() select { - case <-done: + case res := <-done: + return res, nil case <-time.After(timeout): didTimeout = true return "", fmt.Errorf("timeout waiting for response to statusgo.CallPrivateRPC; payload \"%s\"", string(msgJson)) } - - return responseJson, nil } func CallPrivateMethod(method string, params []interface{}) (string, error) { return CallPrivateMethodWithTimeout(method, params, 60*time.Second) } +func CallPrivateMethodAndGetT[T any](method string, params []interface{}) (*T, error) { + resJson, err := CallPrivateMethodWithTimeout(method, params, 60*time.Second) + if err != nil { + return nil, err + } + + var res T + rawJson, err := GetRPCAPIResponseRaw(resJson) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(rawJson, &res); err != nil { + return nil, fmt.Errorf("failed to unmarshal data: %w", err) + } + + return &res, nil +} + type Config struct { HashedPassword string `json:"hashedPassword"` NodeConfigFile string `json:"nodeConfigFile"` @@ -359,29 +465,37 @@ type jsonError struct { } func GetRPCAPIResponse[T any](responseJson string, res T) error { - errApiResponse := jsonrpcErrorResponse{} - err := json.Unmarshal([]byte(responseJson), &errApiResponse) - if err == nil && errApiResponse.Error.Code != 0 { - return fmt.Errorf("API error: %#v", errApiResponse.Error) - } - - apiResponse := jsonrpcSuccessfulResponse{} - err = json.Unmarshal([]byte(responseJson), &apiResponse) - if err != nil { - return fmt.Errorf("failed to unmarshal jsonrpcSuccessfulResponse: %w", err) - } - typeOfT := reflect.TypeOf(res) kindOfT := typeOfT.Kind() - // Check for valid types: pointer, slice, map if kindOfT != reflect.Ptr && kindOfT != reflect.Slice && kindOfT != reflect.Map { return fmt.Errorf("type T must be a pointer, slice, or map") } - if err := json.Unmarshal(apiResponse.Result, &res); err != nil { + rawJson, err := GetRPCAPIResponseRaw(responseJson) + if err != nil { + return err + } + + if err := json.Unmarshal(rawJson, &res); err != nil { return fmt.Errorf("failed to unmarshal data: %w", err) } return nil } + +func GetRPCAPIResponseRaw(responseJson string) (json.RawMessage, error) { + errApiResponse := jsonrpcErrorResponse{} + err := json.Unmarshal([]byte(responseJson), &errApiResponse) + if err == nil && errApiResponse.Error.Code != 0 { + return nil, fmt.Errorf("API error: %#v", errApiResponse.Error) + } + + apiResponse := jsonrpcSuccessfulResponse{} + err = json.Unmarshal([]byte(responseJson), &apiResponse) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal jsonrpcSuccessfulResponse: %w", err) + } + + return apiResponse.Result, nil +} diff --git a/test/status-go/integration/wallet/activityfiltering_test.go b/test/status-go/integration/wallet/activityfiltering_test.go index e4f6553492..6ad138683f 100644 --- a/test/status-go/integration/wallet/activityfiltering_test.go +++ b/test/status-go/integration/wallet/activityfiltering_test.go @@ -4,9 +4,11 @@ package wallet import ( + "encoding/json" "testing" "time" + eth "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" "github.com/status-im/status-desktop/test/status-go/integration/helpers" @@ -14,8 +16,8 @@ import ( "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/services/wallet/activity" "github.com/status-im/status-go/services/wallet/common" + "github.com/status-im/status-go/services/wallet/transfer" "github.com/status-im/status-go/services/wallet/walletevent" - "github.com/status-im/status-go/transactions" ) // TestActivityIncrementalUpdates_NoFilterNewPendingTransactions tests that a pending transaction is created, then updated and finally deleted. @@ -23,34 +25,83 @@ func TestActivityIncrementalUpdates_NoFilterNewPendingTransactions(t *testing.T) td, close := setupAccountsAndTransactions(t) defer close() - _, err := helpers.CallPrivateMethod("wallet_startActivityFilterSession", []interface{}{[]types.Address{td.sender.Address}, false, []common.ChainID{5}, activity.Filter{}, 3}) + rawSessionID, err := helpers.CallPrivateMethodAndGetT[int32]("wallet_startActivityFilterSession", []interface{}{[]types.Address{td.sender.Address}, false, []common.ChainID{5}, activity.Filter{}, 3}) require.NoError(t, err) + require.NotNil(t, rawSessionID) + sessionID := activity.SessionID(*rawSessionID) // Confirm async filtering results - filterRes, err := helpers.WaitForWalletEvents[activity.FilterResponse]( - td.eventQueue, []walletevent.EventType{activity.EventActivityFilteringDone}, - 5*time.Second, - ) + res, err := helpers.WaitForWalletEventGetPayload[activity.FilterResponse](td.eventQueue, activity.EventActivityFilteringDone, 5*time.Second) require.NoError(t, err) - res := filterRes[0] require.Equal(t, activity.ErrorCodeSuccess, res.ErrorCode) require.Equal(t, 3, len(res.Activities)) // Trigger updating of activity results sendTransaction(t, td) - // Wait for EventActivitySessionUpdated signal triggered by the EventPendingTransactionUpdate - update, err := helpers.WaitForWalletEvent[activity.SessionUpdate](td.eventQueue, activity.EventActivitySessionUpdated, 2*time.Second) + // Wait for EventActivitySessionUpdated signal triggered by the first EventPendingTransactionUpdate + update, err := helpers.WaitForWalletEventGetPayload[activity.SessionUpdate](td.eventQueue, activity.EventActivitySessionUpdated, 60*time.Second) require.NoError(t, err) - require.Equal(t, 1, len(update.NewEntries)) + require.NotNil(t, update.HasNewEntries) + require.True(t, *update.HasNewEntries) - // Step x: Trigger downloading of the new transaction ... - _, err = helpers.CallPrivateMethodWithTimeout("wallet_checkRecentHistoryForChainIDs", []interface{}{[]uint64{5}, []types.Address{td.sender.Address, td.recipient.Address}}, 2*time.Second) + // TODO #12120 check EventActivitySessionUpdated due to transactions.EventPendingTransactionStatusChanged + // statusPayload, err := helpers.WaitForWalletEventGetPayload[transactions.StatusChangedPayload](td.eventQueue, , 60*time.Second) + // require.NoError(t, err) + // require.Equal(t, transactions.Success, statusPayload.Status) + + // Start history download to cleanup pending transactions + _, err = helpers.CallPrivateMethod("wallet_checkRecentHistoryForChainIDs", []interface{}{[]uint64{5}, []types.Address{td.sender.Address, td.recipient.Address}}) require.NoError(t, err) - // ... and wait for the new transaction download to trigger deletion from pending_transactions - updatePayload, err := helpers.WaitForWalletEvent[transactions.PendingTxUpdatePayload]( - td.eventQueue, transactions.EventPendingTransactionUpdate, 120*time.Second) + downloadDoneFn := helpers.WaitForTxDownloaderToFinishForAccountsCondition(t, []eth.Address{eth.Address(td.sender.Address), eth.Address(td.recipient.Address)}) + + update = nil + // Wait for EventRecentHistoryReady. + // It is expected that downloading will generate a EventPendingTransactionUpdate that in turn will generate a second EventActivitySessionUpdated signal marked by the update non nil value + _, err = helpers.WaitForWalletEventsWithOptionals( + td.eventQueue, + []walletevent.EventType{transfer.EventRecentHistoryReady}, + 120*time.Second, + func(e *walletevent.Event) bool { + if e.Type == activity.EventActivitySessionUpdated { + var parsedPayload activity.SessionUpdate + err := json.Unmarshal(([]byte)(e.Message), &parsedPayload) + require.NoError(t, err) + update = &parsedPayload + + // TODO #12120 enable after implementing remove and update + // require.NotNil(t, update.HasNewEntries) + // require.True(t, *update.HasNewEntries) + // require.NotNil(t, update.Removed) + // require.True(t, *update.Removed) + return false + } else if e.Type == transfer.EventFetchingHistoryError { + require.Fail(t, "History download failed") + return false + } else if downloadDoneFn(e) { + return true + } + return false + }, + []walletevent.EventType{activity.EventActivitySessionUpdated, transfer.EventFetchingHistoryError}, + ) require.NoError(t, err) - require.Equal(t, true, updatePayload.Deleted) + require.NotNil(t, update, "EventActivitySessionUpdated signal WASN'T triggered by the second EventPendingTransactionUpdate during history download") + require.NotNil(t, update.HasNewEntries) + require.True(t, *update.HasNewEntries) + + // Start history download to cleanup pending transactions + _, err = helpers.CallPrivateMethodAndGetT[interface{}]("wallet_resetFilterSession", []interface{}{sessionID, 3}) + require.NoError(t, err) + + updatedRes, err := helpers.WaitForWalletEventsGetMap(td.eventQueue, []walletevent.EventType{activity.EventActivityFilteringDone}, 1*time.Second) + require.NoError(t, err) + require.Equal(t, activity.ErrorCodeSuccess, activity.ErrorCode(updatedRes[0].JsonData["errorCode"].(float64))) + activitiesList := updatedRes[0].JsonData["activities"].([]interface{}) + require.Equal(t, 3, len(activitiesList)) + firstActivity := activitiesList[0].(map[string]interface{}) + isNew, found := firstActivity["isNew"] + require.True(t, found) + require.True(t, isNew.(bool)) } diff --git a/test/status-go/integration/wallet/common.go b/test/status-go/integration/wallet/common.go index e5a659d5e8..e6a56cfc12 100644 --- a/test/status-go/integration/wallet/common.go +++ b/test/status-go/integration/wallet/common.go @@ -37,13 +37,13 @@ func setupAccountsAndTransactions(t *testing.T) (td testUserData, close func()) require.Greater(t, len(watchAccounts), 0) return testUserData{ - opAccounts[0], - watchAccounts[0], - conf.HashedPassword, - eventQueue, - }, func() { - helpers.Logout(t) - } + opAccounts[0], + watchAccounts[0], + conf.HashedPassword, + eventQueue, + }, func() { + helpers.Logout(t) + } } // sendTransaction generates multi_transactions and pending entries then it creates and publishes a transaction diff --git a/test/status-go/integration/wallet/pendingtransactions_test.go b/test/status-go/integration/wallet/pendingtransactions_test.go index bae892e463..1d754be02e 100644 --- a/test/status-go/integration/wallet/pendingtransactions_test.go +++ b/test/status-go/integration/wallet/pendingtransactions_test.go @@ -7,11 +7,13 @@ import ( "testing" "time" + eth "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" "github.com/status-im/status-desktop/test/status-go/integration/helpers" "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/services/wallet/transfer" "github.com/status-im/status-go/services/wallet/walletevent" "github.com/status-im/status-go/transactions" ) @@ -23,21 +25,44 @@ func TestPendingTx_NotificationStatus(t *testing.T) { sendTransaction(t, td) - // Start history download ... - _, err := helpers.CallPrivateMethod("wallet_checkRecentHistoryForChainIDs", []interface{}{[]uint64{5}, []types.Address{td.sender.Address, td.recipient.Address}}) - require.NoError(t, err) - - // ... and wait for the new transaction download to trigger deletion from pending_transactions - updatePayloads, err := helpers.WaitForWalletEvents[transactions.PendingTxUpdatePayload]( + // Wait for transaction to be included in block + confirmationPayloads, err := helpers.WaitForWalletEventsGetMap( td.eventQueue, []walletevent.EventType{ transactions.EventPendingTransactionUpdate, - transactions.EventPendingTransactionUpdate, + transactions.EventPendingTransactionStatusChanged, }, 60*time.Second, ) require.NoError(t, err) - // Validate that we received both add and delete event - require.False(t, updatePayloads[0].Deleted) - require.True(t, updatePayloads[1].Deleted) + // Validate that we received update event + for _, payload := range confirmationPayloads { + if payload.EventName == transactions.EventPendingTransactionUpdate { + require.False(t, payload.JsonData["deleted"].(bool)) + } else { + require.Equal(t, transactions.Success, payload.JsonData["status"].(transactions.TxStatus)) + } + } + + // Start history download ... + _, err = helpers.CallPrivateMethod("wallet_checkRecentHistoryForChainIDs", []interface{}{[]uint64{5}, []types.Address{td.sender.Address, td.recipient.Address}}) + require.NoError(t, err) + + downloadDoneFn := helpers.WaitForTxDownloaderToFinishForAccountsCondition(t, []eth.Address{eth.Address(td.sender.Address), eth.Address(td.recipient.Address)}) + + // ... and wait for the new transaction download to trigger deletion from pending_transactions + _, err = helpers.WaitForWalletEventsWithOptionals( + td.eventQueue, + []walletevent.EventType{transfer.EventRecentHistoryReady}, + 60*time.Second, + func(e *walletevent.Event) bool { + if e.Type == transfer.EventFetchingHistoryError { + require.Fail(t, "History download failed") + return false + } + return downloadDoneFn(e) + }, + []walletevent.EventType{transfer.EventFetchingHistoryError}, + ) + require.NoError(t, err) } diff --git a/vendor/status-go b/vendor/status-go index 1c42c07760..e9ff0fbefe 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit 1c42c077603dbd37cb10ebc5cccb755925fc812b +Subproject commit e9ff0fbefe11d57f03f0ef6cb7d30cb0b5ebe44e