2024-01-04 04:37:42 +00:00
package helpers
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"testing"
"time"
2024-02-08 12:47:09 +00:00
"github.com/ethereum/go-ethereum/common"
2024-01-04 04:37:42 +00:00
"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"
2023-12-17 04:18:20 +00:00
"github.com/stretchr/testify/require"
2024-01-04 04:37:42 +00:00
)
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
}
2023-12-17 04:18:20 +00:00
func Logout ( t * testing . T ) {
err := logout ( )
require . NoError ( t , err )
}
2024-01-04 04:37:42 +00:00
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 )
}
}
}
2024-02-08 12:47:09 +00:00
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.
2024-02-08 12:49:12 +00:00
// If a condition is provided, only returning true on the respective call will discard that event.
2024-02-08 12:47:09 +00:00
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" )
}
2023-12-17 04:18:20 +00:00
2024-02-08 12:47:09 +00:00
startTime := time . Now ( )
expected := make ( [ ] walletevent . EventType , len ( eventNames ) )
copy ( expected , eventNames )
walletEvents = make ( [ ] * walletevent . Event , 0 , len ( eventNames ) )
2023-12-17 04:18:20 +00:00
2024-02-08 12:47:09 +00:00
infiniteLoop :
2024-01-04 04:37:42 +00:00
for {
2024-02-08 12:47:09 +00:00
toWait := timeout - time . Since ( startTime )
if toWait <= 0 {
return nil , fmt . Errorf ( "timeout waiting for events %+v" , expected )
}
event , err := WaitForEvent ( eventQueue , WalletEvent , toWait )
2024-01-04 04:37:42 +00:00
if err != nil {
2024-02-08 12:47:09 +00:00
return nil , fmt . Errorf ( "error waiting for events %+v: %w" , expected , err )
2024-01-04 04:37:42 +00:00
}
walletEvent , ok := event . Payload . ( walletevent . Event )
if ! ok {
return nil , errors . New ( "event payload is not a wallet event" )
}
2024-02-08 12:47:09 +00:00
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
2023-12-17 04:18:20 +00:00
}
}
2024-02-08 12:47:09 +00:00
for _ , event := range optionalEventNames {
if walletEvent . Type == event && condition != nil {
_ = condition ( & walletEvent )
2023-12-17 04:18:20 +00:00
}
2024-02-08 12:47:09 +00:00
}
}
}
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
2024-01-04 04:37:42 +00:00
}
}
2024-02-08 12:47:09 +00:00
payloads [ i ] = payloadMapRes {
EventName : payload . eventName ,
JsonData : mapPayload ,
}
2024-01-04 04:37:42 +00:00
}
2024-02-08 12:47:09 +00:00
return payloads , nil
2024-01-04 04:37:42 +00:00
}
2024-02-08 12:47:09 +00:00
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 )
2023-12-17 04:18:20 +00:00
if err != nil {
return nil , err
}
2024-02-08 12:47:09 +00:00
return newPayload , nil
}
2024-04-02 13:27:21 +00:00
// WaitForTxDownloaderToFinishForAccountsCondition returns a stateful condition function that checks that at least on account that has been seen with the events until the entire list is seen.
// The loadBlocksAndTransfersCommand.fetchHistoryBlocksForAccount reports only for one account history ready, even though the downloaded history might contain other accounts.
2024-02-08 12:47:09 +00:00
func WaitForTxDownloaderToFinishForAccountsCondition ( t * testing . T , accounts [ ] common . Address ) func ( walletEvent * walletevent . Event ) bool {
return func ( walletEvent * walletevent . Event ) bool {
for _ , acc := range walletEvent . Accounts {
2024-04-02 13:27:21 +00:00
for _ , a := range accounts {
2024-02-08 12:47:09 +00:00
if acc == a {
2024-04-02 13:27:21 +00:00
return true
2024-02-08 12:47:09 +00:00
}
}
}
return false
}
2023-12-17 04:18:20 +00:00
}
2024-01-04 04:37:42 +00:00
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
}
2023-12-17 04:18:20 +00:00
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
}
2024-01-04 04:37:42 +00:00
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" `
}
2023-12-17 04:18:20 +00:00
func CallPrivateMethodWithTimeout ( method string , params [ ] interface { } , timeout time . Duration ) ( string , error ) {
2024-01-04 04:37:42 +00:00
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
}
2023-12-17 04:18:20 +00:00
didTimeout := false
2024-02-08 12:47:09 +00:00
done := make ( chan string )
2023-12-17 04:18:20 +00:00
go func ( ) {
2024-02-08 12:47:09 +00:00
responseJson := statusgo . CallPrivateRPC ( string ( msgJson ) )
2023-12-17 04:18:20 +00:00
if didTimeout {
log . Warn ( "Call to CallPrivateRPC returned after timeout" , "payload" , string ( msgJson ) )
return
}
2024-02-08 12:47:09 +00:00
done <- responseJson
2023-12-17 04:18:20 +00:00
} ( )
select {
2024-02-08 12:47:09 +00:00
case res := <- done :
return res , nil
2023-12-17 04:18:20 +00:00
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 )
2024-01-04 04:37:42 +00:00
}
2024-02-08 12:47:09 +00:00
func CallPrivateMethodAndGetT [ T any ] ( method string , params [ ] interface { } ) ( * T , error ) {
2024-03-29 17:10:58 +00:00
return CallPrivateMethodAndGetTWithTimeout [ T ] ( method , params , 60 * time . Second )
}
func CallPrivateMethodAndGetTWithTimeout [ T any ] ( method string , params [ ] interface { } , timeout time . Duration ) ( * T , error ) {
resJson , err := CallPrivateMethodWithTimeout ( method , params , timeout )
2024-02-08 12:47:09 +00:00
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
}
2024-04-02 13:27:21 +00:00
func CallPrivateMethodAndGetSliceOfT [ T any ] ( method string , params [ ] interface { } ) ( [ ] T , error ) {
return CallPrivateMethodAndGetSliceOfTWithTimeout [ T ] ( method , params , 60 * time . Second )
}
func CallPrivateMethodAndGetSliceOfTWithTimeout [ T any ] ( method string , params [ ] interface { } , timeout time . Duration ) ( [ ] T , error ) {
resJson , err := CallPrivateMethodWithTimeout ( method , params , timeout )
if err != nil {
return nil , err
}
res := make ( [ ] T , 0 )
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
}
2024-02-08 12:47:09 +00:00
2024-01-04 04:37:42 +00:00
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
}
2023-12-17 04:18:20 +00:00
// GetCAPIResponse expects res to be a pointer to a struct, a pointer to a slice or a pointer to a map for marshaling
2024-01-04 04:37:42 +00:00
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" )
}
2024-02-08 12:47:09 +00:00
rawJson , err := GetRPCAPIResponseRaw ( responseJson )
if err != nil {
return err
}
if err := json . Unmarshal ( rawJson , & res ) ; err != nil {
2024-01-04 04:37:42 +00:00
return fmt . Errorf ( "failed to unmarshal data: %w" , err )
}
return nil
}
2024-02-08 12:47:09 +00:00
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
}