mirror of
https://github.com/status-im/status-desktop.git
synced 2025-02-22 11:38:57 +00:00
Switch the activity filter to use the new session-based API that deliver incremental updates to the current filter. Drop the old quick win listening for individual change events and use the unified API instead. The new transactions (on-top) trigger the old "new transactions" buttons that trigger reset of the current filter and the top new transacitons highlighted. Highlight mixed changes (not new on top) as they come in Highlight new changes on filter reset Closes #12120
503 lines
14 KiB
Go
503 lines
14 KiB
Go
package helpers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"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"
|
|
"github.com/status-im/status-go/services/wallet/walletevent"
|
|
"github.com/status-im/status-go/signal"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type StatusGoEventName string
|
|
|
|
const NodeReadyEvent StatusGoEventName = "nodeReady"
|
|
const WalletEvent StatusGoEventName = "wallet"
|
|
|
|
type GoEvent struct {
|
|
Name StatusGoEventName `json:"name"`
|
|
Payload interface{} `json:"payload"`
|
|
}
|
|
|
|
type envelope struct {
|
|
Type string `json:"type"`
|
|
Event json.RawMessage `json:"event"` // Use json.RawMessage to delay parsing
|
|
}
|
|
|
|
func signalHandler(eventQueue chan GoEvent, jsonEvent string) {
|
|
envelope := envelope{}
|
|
err := json.Unmarshal([]byte(jsonEvent), &envelope)
|
|
if err != nil {
|
|
apiResponse := statusgo.APIResponse{}
|
|
err = json.Unmarshal([]byte(jsonEvent), &apiResponse)
|
|
if err != nil {
|
|
log.Error("Error parsing the signal event: ", err)
|
|
} else if apiResponse.Error != "" {
|
|
log.Error("Error from status-go: ", apiResponse.Error)
|
|
} else {
|
|
log.Error("Unknown JSON content for event", jsonEvent)
|
|
}
|
|
return
|
|
}
|
|
|
|
if envelope.Type == signal.EventNodeReady {
|
|
eventQueue <- GoEvent{Name: NodeReadyEvent, Payload: string(envelope.Event)}
|
|
} else if envelope.Type == string(WalletEvent) {
|
|
walletEvent := walletevent.Event{}
|
|
err := json.Unmarshal(envelope.Event, &walletEvent)
|
|
if err != nil {
|
|
log.Error("Error parsing the wallet event: ", err)
|
|
return
|
|
}
|
|
eventQueue <- GoEvent{Name: WalletEvent, Payload: walletEvent}
|
|
}
|
|
}
|
|
|
|
func LoginToTestAccount(t *testing.T) (eventQueue chan GoEvent, config *Config, l log.Logger) {
|
|
l = log.New()
|
|
l.SetHandler(log.CallerFileHandler(log.StreamHandler(os.Stdout, log.TerminalFormat(false))))
|
|
|
|
// Setup status-go logger
|
|
log.Root().SetHandler(log.CallerFileHandler(log.StdoutHandler))
|
|
|
|
eventQueue = make(chan GoEvent, 10000)
|
|
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
|
|
signalHandler(eventQueue, jsonEvent)
|
|
})
|
|
|
|
conf, nodeConfigJson, userFolder, err := processConfigArgs("./.integration_tests_config.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
config = conf
|
|
|
|
// Login to first account
|
|
err = loginToAccount(config.HashedPassword, userFolder, nodeConfigJson)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
func Logout(t *testing.T) {
|
|
err := logout()
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func WaitForEvent(eventQueue chan GoEvent, eventName StatusGoEventName, timeout time.Duration) (event *GoEvent, err error) {
|
|
for {
|
|
select {
|
|
case event := <-eventQueue:
|
|
if event.Name == eventName {
|
|
return &event, nil
|
|
}
|
|
case <-time.After(timeout):
|
|
return nil, fmt.Errorf("timeout waiting for event %s", eventName)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// WaitForWalletEvents waits for the given events to be received on the eventQueue.
|
|
// It returns the wallet events in the order they are received.
|
|
// If a condition is provided, only returning true on the respective call will discard that event.
|
|
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 {
|
|
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, fmt.Errorf("error waiting for events %+v: %w", expected, err)
|
|
}
|
|
|
|
walletEvent, ok := event.Payload.(walletevent.Event)
|
|
if !ok {
|
|
return nil, errors.New("event payload is not a wallet event")
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
for _, event := range optionalEventNames {
|
|
if walletEvent.Type == event && condition != nil {
|
|
_ = condition(&walletEvent)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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 {
|
|
absUserFolder, err := filepath.Abs(userFolder)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
accountsJson := statusgo.OpenAccounts(absUserFolder)
|
|
accounts := make([]multiaccounts.Account, 0)
|
|
err = GetCAPIResponse(accountsJson, &accounts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(accounts) == 0 {
|
|
return fmt.Errorf("no accounts found")
|
|
}
|
|
|
|
account := accounts[0]
|
|
keystorePath := filepath.Join(filepath.Join(absUserFolder, "keystore/"), account.KeyUID)
|
|
initKeystoreJson := statusgo.InitKeystore(keystorePath)
|
|
apiResponse := statusgo.APIResponse{}
|
|
err = GetCAPIResponse(initKeystoreJson, &apiResponse)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
//serialize account of type multiaccounts.Account
|
|
accountJson, err := json.Marshal(account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
loginJson := statusgo.LoginWithConfig(string(accountJson), hashedPassword, nodeConfigJson)
|
|
err = GetCAPIResponse(loginJson, &apiResponse)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func logout() error {
|
|
logoutJson := statusgo.Logout()
|
|
apiResponse := statusgo.APIResponse{}
|
|
err := GetCAPIResponse(logoutJson, &apiResponse)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if apiResponse.Error != "" {
|
|
return fmt.Errorf("API error: %s", apiResponse.Error)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type jsonrpcMessage struct {
|
|
Version string `json:"jsonrpc"`
|
|
ID json.RawMessage `json:"id"`
|
|
}
|
|
|
|
type jsonrpcRequest struct {
|
|
jsonrpcMessage
|
|
ChainID uint64 `json:"chainId"`
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params,omitempty"`
|
|
}
|
|
|
|
func CallPrivateMethodWithTimeout(method string, params []interface{}, timeout time.Duration) (string, error) {
|
|
var paramsJson json.RawMessage
|
|
var err error
|
|
if params != nil {
|
|
paramsJson, err = json.Marshal(params)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
msg := jsonrpcRequest{
|
|
jsonrpcMessage: jsonrpcMessage{
|
|
Version: "2.0",
|
|
},
|
|
Method: method,
|
|
Params: paramsJson,
|
|
}
|
|
|
|
msgJson, err := json.Marshal(msg)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
didTimeout := false
|
|
done := make(chan string)
|
|
go func() {
|
|
responseJson := statusgo.CallPrivateRPC(string(msgJson))
|
|
if didTimeout {
|
|
log.Warn("Call to CallPrivateRPC returned after timeout", "payload", string(msgJson))
|
|
return
|
|
}
|
|
|
|
done <- responseJson
|
|
}()
|
|
|
|
select {
|
|
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))
|
|
}
|
|
}
|
|
|
|
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"`
|
|
DataDir string `json:"dataDir"`
|
|
}
|
|
|
|
// processConfigArgs expects that configFilePath points to a JSON file that contains a Config struct
|
|
// For now this are for developer to manually run them using an existing user folder.
|
|
// TODO: ideally we would generate a temporary user folder to be used in the entire suite.
|
|
func processConfigArgs(configFilePath string) (config *Config, nodeConfigJson string, userFolder string, err error) {
|
|
config = &Config{}
|
|
// parse config file
|
|
configFile, err := os.Open(configFilePath)
|
|
if err != nil {
|
|
return nil, "", "", err
|
|
}
|
|
defer configFile.Close()
|
|
|
|
jsonParser := json.NewDecoder(configFile)
|
|
if err = jsonParser.Decode(&config); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
nodeConfigFile, err := os.Open(config.NodeConfigFile)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer nodeConfigFile.Close()
|
|
|
|
nodeConfigData, err := io.ReadAll(nodeConfigFile)
|
|
if err == nil {
|
|
nodeConfigJson = string(nodeConfigData)
|
|
}
|
|
|
|
userFolder = config.DataDir
|
|
|
|
return
|
|
}
|
|
|
|
// GetCAPIResponse expects res to be a pointer to a struct, a pointer to a slice or a pointer to a map for marshaling
|
|
func GetCAPIResponse[T any](responseJson string, res T) error {
|
|
apiResponse := statusgo.APIResponse{}
|
|
err := json.Unmarshal([]byte(responseJson), &apiResponse)
|
|
if err == nil {
|
|
if apiResponse.Error != "" {
|
|
return fmt.Errorf("API error: %s", apiResponse.Error)
|
|
}
|
|
}
|
|
|
|
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([]byte(responseJson), &res); err != nil {
|
|
return fmt.Errorf("failed to unmarshal data: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type jsonrpcSuccessfulResponse struct {
|
|
jsonrpcMessage
|
|
Result json.RawMessage `json:"result"`
|
|
}
|
|
|
|
type jsonrpcErrorResponse struct {
|
|
jsonrpcMessage
|
|
Error jsonError `json:"error"`
|
|
}
|
|
|
|
// jsonError represents Error message for JSON-RPC responses.
|
|
type jsonError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
}
|
|
|
|
func GetRPCAPIResponse[T any](responseJson string, res T) error {
|
|
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")
|
|
}
|
|
|
|
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
|
|
}
|