status-im/status-go

View on GitHub
server/pairing/sync_device_test.go

Summary

Maintainability
C
7 hrs
Test Coverage
package pairing

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "os"
    "path/filepath"
    "reflect"
    "strings"
    "testing"
    "time"

    "go.uber.org/zap"

    "github.com/status-im/status-go/common/dbsetup"
    "github.com/status-im/status-go/eth-node/crypto"
    "github.com/status-im/status-go/protocol"
    "github.com/status-im/status-go/protocol/encryption/multidevice"
    "github.com/status-im/status-go/protocol/tt"

    "github.com/google/uuid"
    "github.com/stretchr/testify/require"
    "github.com/stretchr/testify/suite"

    "github.com/status-im/status-go/account/generator"
    "github.com/status-im/status-go/api"
    "github.com/status-im/status-go/eth-node/types"
    "github.com/status-im/status-go/multiaccounts"
    "github.com/status-im/status-go/multiaccounts/accounts"
    "github.com/status-im/status-go/multiaccounts/settings"
    "github.com/status-im/status-go/params"
    "github.com/status-im/status-go/protocol/common"
    "github.com/status-im/status-go/protocol/identity/alias"
    "github.com/status-im/status-go/protocol/protobuf"
    "github.com/status-im/status-go/protocol/requests"
    accservice "github.com/status-im/status-go/services/accounts"
    "github.com/status-im/status-go/services/browsers"
)

const (
    pathWalletRoot          = "m/44'/60'/0'/0"
    pathEIP1581             = "m/43'/60'/1581'"
    pathDefaultChat         = pathEIP1581 + "/0'/0"
    pathDefaultWallet       = pathWalletRoot + "/0"
    currentNetwork          = "mainnet_rpc"
    socialLinkURL           = "https://github.com/status-im"
    ensUsername             = "bob.stateofus.eth"
    ensChainID              = 1
    publicChatID            = "localpairtest"
    profileKeypairMnemonic  = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"
    seedKeypairMnemonic     = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
    profileKeypairMnemonic1 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about about"
    seedKeypairMnemonic1    = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about abandon"
    path0                   = "m/44'/60'/0'/0/0"
    path1                   = "m/44'/60'/0'/0/1"
    expectedKDFIterations   = 1024
)

var paths = []string{pathWalletRoot, pathEIP1581, pathDefaultChat, pathDefaultWallet}

func TestSyncDeviceSuite(t *testing.T) {
    suite.Run(t, new(SyncDeviceSuite))
}

type SyncDeviceSuite struct {
    suite.Suite
    logger   *zap.Logger
    password string
    tmpdir   string
}

func (s *SyncDeviceSuite) SetupTest() {
    s.logger = tt.MustCreateTestLogger()
    s.password = "password"
    s.tmpdir = s.T().TempDir()
}

func (s *SyncDeviceSuite) prepareBackendWithAccount(mnemonic, tmpdir string) *api.GethStatusBackend {
    backend := s.prepareBackendWithoutAccount(tmpdir)
    accountManager := backend.AccountManager()
    accGenerator := accountManager.AccountsGenerator()

    var (
        generatedAccountInfo generator.GeneratedAndDerivedAccountInfo
        err                  error
    )
    if len(mnemonic) > 0 {
        generatedAccountInfo.GeneratedAccountInfo, err = accGenerator.ImportMnemonic(mnemonic, "")
        require.NoError(s.T(), err)
        generatedAccountInfo.Derived, err = accGenerator.DeriveAddresses(generatedAccountInfo.ID, paths)
        require.NoError(s.T(), err)
    } else {
        generatedAccountInfos, err := accGenerator.GenerateAndDeriveAddresses(12, 1, "", paths)
        require.NoError(s.T(), err)
        generatedAccountInfo = generatedAccountInfos[0]
    }
    account := multiaccounts.Account{
        KeyUID:        generatedAccountInfo.KeyUID,
        KDFIterations: dbsetup.ReducedKDFIterationsNumber,
    }
    err = accountManager.InitKeystore(filepath.Join(tmpdir, keystoreDir, account.KeyUID))
    require.NoError(s.T(), err)
    err = backend.OpenAccounts()
    require.NoError(s.T(), err)
    derivedAddresses := generatedAccountInfo.Derived
    _, err = accGenerator.StoreDerivedAccounts(generatedAccountInfo.ID, s.password, paths)
    require.NoError(s.T(), err)

    settings, err := defaultSettings(generatedAccountInfo.GeneratedAccountInfo, derivedAddresses, nil)
    require.NoError(s.T(), err)

    account.Name = settings.Name

    nodeConfig, err := nodeConfigForLocalPairSync(settings.InstallationID, account.KeyUID, tmpdir)
    nodeConfig.RootDataDir = tmpdir
    require.NoError(s.T(), err)
    require.NoError(s.T(), setDefaultNodeConfig(nodeConfig))

    walletDerivedAccount := derivedAddresses[pathDefaultWallet]
    walletAccount := &accounts.Account{
        PublicKey: types.Hex2Bytes(walletDerivedAccount.PublicKey),
        KeyUID:    generatedAccountInfo.KeyUID,
        Address:   types.HexToAddress(walletDerivedAccount.Address),
        ColorID:   "",
        Wallet:    true,
        Path:      pathDefaultWallet,
        Name:      "Ethereum account",
    }

    chatDerivedAccount := derivedAddresses[pathDefaultChat]
    chatAccount := &accounts.Account{
        PublicKey: types.Hex2Bytes(chatDerivedAccount.PublicKey),
        KeyUID:    generatedAccountInfo.KeyUID,
        Address:   types.HexToAddress(chatDerivedAccount.Address),
        Name:      settings.Name,
        Chat:      true,
        Path:      pathDefaultChat,
    }

    accounts := []*accounts.Account{walletAccount, chatAccount}
    err = backend.StartNodeWithAccountAndInitialConfig(account, s.password, *settings, nodeConfig, accounts, nil)
    require.NoError(s.T(), err)
    multiaccounts, err := backend.GetAccounts()
    require.NoError(s.T(), err)
    require.NotEmpty(s.T(), multiaccounts[0].ColorHash)

    return backend
}

func (s *SyncDeviceSuite) prepareBackendWithoutAccount(tmpdir string) *api.GethStatusBackend {
    backend := api.NewGethStatusBackend()
    backend.UpdateRootDataDir(tmpdir)
    return backend
}

func (s *SyncDeviceSuite) pairAccounts(serverBackend *api.GethStatusBackend, serverDir string,
    clientBackend *api.GethStatusBackend, clientDir string) {

    // Start sender server

    serverActiveAccount, err := serverBackend.GetActiveAccount()
    require.NoError(s.T(), err)

    serverKeystorePath := filepath.Join(serverDir, keystoreDir, serverActiveAccount.KeyUID)
    serverConfig := &SenderServerConfig{
        SenderConfig: &SenderConfig{
            KeystorePath: serverKeystorePath,
            DeviceType:   "desktop",
            KeyUID:       serverActiveAccount.KeyUID,
            Password:     s.password,
        },
        ServerConfig: new(ServerConfig),
    }

    configBytes, err := json.Marshal(serverConfig)
    require.NoError(s.T(), err)

    connectionString, err := StartUpSenderServer(serverBackend, string(configBytes))
    require.NoError(s.T(), err)

    // Start receiving client

    err = clientBackend.AccountManager().InitKeystore(filepath.Join(clientDir, keystoreDir))
    require.NoError(s.T(), err)

    err = clientBackend.OpenAccounts()
    require.NoError(s.T(), err)

    clientNodeConfig, err := nodeConfigForLocalPairSync(uuid.New().String(), "", clientDir)
    require.NoError(s.T(), err)

    clientKeystoreDir := filepath.Join(clientDir, keystoreDir)
    clientPayloadSourceConfig := ReceiverClientConfig{
        ReceiverConfig: &ReceiverConfig{
            KeystorePath:          clientKeystoreDir,
            DeviceType:            "desktop",
            KDFIterations:         expectedKDFIterations,
            NodeConfig:            clientNodeConfig,
            SettingCurrentNetwork: currentNetwork,
        },
        ClientConfig: new(ClientConfig),
    }
    clientNodeConfig.RootDataDir = clientDir

    clientConfigBytes, err := json.Marshal(clientPayloadSourceConfig)
    require.NoError(s.T(), err)

    err = StartUpReceivingClient(clientBackend, connectionString, string(clientConfigBytes))
    require.NoError(s.T(), err)

    require.True(s.T(), serverBackend.Messenger().HasPairedDevices())
    require.True(s.T(), clientBackend.Messenger().HasPairedDevices())
}

func (s *SyncDeviceSuite) sendContactRequest(request *requests.SendContactRequest, messenger *protocol.Messenger) {
    senderPublicKey := common.PubkeyToHex(messenger.IdentityPublicKey())
    s.logger.Info("sendContactRequest", zap.String("sender", senderPublicKey), zap.String("receiver", request.ID))

    resp, err := messenger.SendContactRequest(context.Background(), request)
    s.Require().NoError(err)
    s.Require().NotNil(resp)
}

func (s *SyncDeviceSuite) receiveContactRequest(messageText string, messenger *protocol.Messenger) *common.Message {
    receiverPublicKey := types.EncodeHex(crypto.FromECDSAPub(messenger.IdentityPublicKey()))
    s.logger.Info("receiveContactRequest", zap.String("receiver", receiverPublicKey))

    // Wait for the message to reach its destination
    resp, err := protocol.WaitOnMessengerResponse(
        messenger,
        func(r *protocol.MessengerResponse) bool {
            return len(r.Contacts) == 1 && len(r.Messages()) == 2 && len(r.ActivityCenterNotifications()) == 1
        },
        "no messages",
    )

    s.Require().NoError(err)
    s.Require().NotNil(resp)

    contactRequest := protocol.FindFirstByContentType(resp.Messages(), protobuf.ChatMessage_CONTACT_REQUEST)
    s.Require().NotNil(contactRequest)

    return contactRequest
}

func (s *SyncDeviceSuite) acceptContactRequest(contactRequest *common.Message, sender *protocol.Messenger, receiver *protocol.Messenger) {
    senderPublicKey := types.EncodeHex(crypto.FromECDSAPub(sender.IdentityPublicKey()))
    receiverPublicKey := types.EncodeHex(crypto.FromECDSAPub(receiver.IdentityPublicKey()))
    s.logger.Info("acceptContactRequest", zap.String("sender", senderPublicKey), zap.String("receiver", receiverPublicKey))

    _, err := receiver.AcceptContactRequest(context.Background(), &requests.AcceptContactRequest{ID: types.Hex2Bytes(contactRequest.ID)})
    s.Require().NoError(err)

    // Wait for the message to reach its destination
    resp, err := protocol.WaitOnMessengerResponse(
        sender,
        func(r *protocol.MessengerResponse) bool {
            return len(r.Contacts) == 1 && len(r.Messages()) == 2 && len(r.ActivityCenterNotifications()) == 1
        },
        "no messages",
    )
    s.Require().NoError(err)
    s.Require().NotNil(resp)
}

func (s *SyncDeviceSuite) checkMutualContact(backend *api.GethStatusBackend, contactPublicKey string) {
    messenger := backend.Messenger()
    contacts := messenger.MutualContacts()
    s.Require().Len(contacts, 1)
    contact := contacts[0]
    s.Require().Equal(contactPublicKey, contact.ID)
    s.Require().Equal(protocol.ContactRequestStateSent, contact.ContactRequestLocalState)
    s.Require().Equal(protocol.ContactRequestStateReceived, contact.ContactRequestRemoteState)
    s.Require().NotNil(contact.DisplayName)
}

func (s *SyncDeviceSuite) TestPairingSyncDeviceClientAsSender() {
    clientTmpDir := filepath.Join(s.tmpdir, "client")
    clientBackend := s.prepareBackendWithAccount("", clientTmpDir)
    serverTmpDir := filepath.Join(s.tmpdir, "server")
    serverBackend := s.prepareBackendWithoutAccount(serverTmpDir)
    defer func() {
        require.NoError(s.T(), serverBackend.Logout())
        require.NoError(s.T(), clientBackend.Logout())
    }()
    ctx := context.TODO()

    err := serverBackend.AccountManager().InitKeystore(filepath.Join(serverTmpDir, keystoreDir))
    require.NoError(s.T(), err)
    err = serverBackend.OpenAccounts()
    require.NoError(s.T(), err)
    serverNodeConfig, err := nodeConfigForLocalPairSync(uuid.New().String(), "", serverTmpDir)
    require.NoError(s.T(), err)
    serverKeystoreDir := filepath.Join(serverTmpDir, keystoreDir)
    serverPayloadSourceConfig := &ReceiverServerConfig{
        ReceiverConfig: &ReceiverConfig{
            NodeConfig:            serverNodeConfig,
            KeystorePath:          serverKeystoreDir,
            DeviceType:            "desktop",
            KDFIterations:         expectedKDFIterations,
            SettingCurrentNetwork: currentNetwork,
        },
        ServerConfig: new(ServerConfig),
    }
    serverNodeConfig.RootDataDir = serverTmpDir
    serverConfigBytes, err := json.Marshal(serverPayloadSourceConfig)
    require.NoError(s.T(), err)
    cs, err := StartUpReceiverServer(serverBackend, string(serverConfigBytes))
    require.NoError(s.T(), err)

    // generate some data for the client
    // generate bookmark
    clientBrowserAPI := clientBackend.StatusNode().BrowserService().APIs()[0].Service.(*browsers.API)
    _, err = clientBrowserAPI.StoreBookmark(ctx, browsers.Bookmark{
        Name: "status.im",
        URL:  "https://status.im",
    })
    require.NoError(s.T(), err)

    // generate ens username
    err = clientBackend.StatusNode().EnsService().API().Add(ctx, ensChainID, ensUsername)
    require.NoError(s.T(), err)
    // generate profile showcase preferences
    profileShowcasePreferences := protocol.DummyProfileShowcasePreferences(false)
    err = clientBackend.Messenger().SetProfileShowcasePreferences(profileShowcasePreferences, false)
    require.NoError(s.T(), err)

    // startup sending client
    clientActiveAccount, err := clientBackend.GetActiveAccount()
    require.NoError(s.T(), err)
    clientKeystorePath := filepath.Join(clientTmpDir, keystoreDir, clientActiveAccount.KeyUID)
    clientPayloadSourceConfig := SenderClientConfig{
        SenderConfig: &SenderConfig{
            KeystorePath: clientKeystorePath,
            DeviceType:   "android",
            KeyUID:       clientActiveAccount.KeyUID,
            Password:     s.password,
        },
        ClientConfig: new(ClientConfig),
    }
    clientConfigBytes, err := json.Marshal(clientPayloadSourceConfig)
    require.NoError(s.T(), err)
    err = StartUpSendingClient(clientBackend, cs, string(clientConfigBytes))
    require.NoError(s.T(), err)

    // check that the server has the same data as the client
    serverBrowserAPI := serverBackend.StatusNode().BrowserService().APIs()[0].Service.(*browsers.API)

    bookmarks, err := serverBrowserAPI.GetBookmarks(ctx)
    require.NoError(s.T(), err)
    require.Equal(s.T(), 1, len(bookmarks))
    require.Equal(s.T(), "status.im", bookmarks[0].Name)

    uds, err := serverBackend.StatusNode().EnsService().API().GetEnsUsernames(ctx)
    require.NoError(s.T(), err)
    require.Equal(s.T(), 1, len(uds))
    require.Equal(s.T(), ensUsername, uds[0].Username)
    require.Equal(s.T(), uint64(ensChainID), uds[0].ChainID)
    require.False(s.T(), uds[0].Removed)
    require.Greater(s.T(), uds[0].Clock, uint64(0))

    serverProfileShowcasePreferences, err := serverBackend.Messenger().GetProfileShowcasePreferences()
    require.NoError(s.T(), err)
    require.True(s.T(), reflect.DeepEqual(profileShowcasePreferences, serverProfileShowcasePreferences))

    serverActiveAccount, err := serverBackend.GetActiveAccount()
    require.NoError(s.T(), err)
    require.Equal(s.T(), serverActiveAccount.Name, clientActiveAccount.Name)
    require.Equal(s.T(), serverActiveAccount.KDFIterations, expectedKDFIterations)

    serverMessenger := serverBackend.Messenger()
    clientMessenger := clientBackend.Messenger()
    require.True(s.T(), serverMessenger.HasPairedDevices())
    require.True(s.T(), clientMessenger.HasPairedDevices())

    err = clientMessenger.DisableInstallation(serverNodeConfig.ShhextConfig.InstallationID)
    require.NoError(s.T(), err)
    require.False(s.T(), clientMessenger.HasPairedDevices())

    clientNodeConfig, err := clientBackend.GetNodeConfig()
    require.NoError(s.T(), err)
    err = serverMessenger.DisableInstallation(clientNodeConfig.ShhextConfig.InstallationID)
    require.NoError(s.T(), err)
    require.False(s.T(), serverMessenger.HasPairedDevices())

    // repeat local pairing, we should expect no error after receiver logged in
    cs, err = StartUpReceiverServer(serverBackend, string(serverConfigBytes))
    require.NoError(s.T(), err)
    err = StartUpSendingClient(clientBackend, cs, string(clientConfigBytes))
    require.NoError(s.T(), err)
    require.True(s.T(), clientMessenger.HasPairedDevices())
    require.True(s.T(), serverMessenger.HasPairedDevices())

    // test if it's okay when account already exist but not logged in
    require.NoError(s.T(), serverBackend.Logout())
    cs, err = StartUpReceiverServer(serverBackend, string(serverConfigBytes))
    require.NoError(s.T(), err)
    err = StartUpSendingClient(clientBackend, cs, string(clientConfigBytes))
    require.NoError(s.T(), err)
}

func (s *SyncDeviceSuite) TestPairingSyncDeviceClientAsReceiver() {
    clientTmpDir := filepath.Join(s.tmpdir, "client")
    clientBackend := s.prepareBackendWithoutAccount(clientTmpDir)
    ctx := context.TODO()

    serverTmpDir := filepath.Join(s.tmpdir, "server")
    serverBackend := s.prepareBackendWithAccount("", serverTmpDir)
    defer func() {
        require.NoError(s.T(), clientBackend.Logout())
        require.NoError(s.T(), serverBackend.Logout())
    }()

    serverActiveAccount, err := serverBackend.GetActiveAccount()
    require.NoError(s.T(), err)
    serverKeystorePath := filepath.Join(serverTmpDir, keystoreDir, serverActiveAccount.KeyUID)
    var config = &SenderServerConfig{
        SenderConfig: &SenderConfig{
            KeystorePath: serverKeystorePath,
            DeviceType:   "desktop",
            KeyUID:       serverActiveAccount.KeyUID,
            Password:     s.password,
        },
        ServerConfig: new(ServerConfig),
    }
    configBytes, err := json.Marshal(config)
    require.NoError(s.T(), err)
    cs, err := StartUpSenderServer(serverBackend, string(configBytes))
    require.NoError(s.T(), err)

    // generate some data for the server
    // generate bookmark
    serverBrowserAPI := serverBackend.StatusNode().BrowserService().APIs()[0].Service.(*browsers.API)
    _, err = serverBrowserAPI.StoreBookmark(ctx, browsers.Bookmark{
        Name: "status.im",
        URL:  "https://status.im",
    })
    require.NoError(s.T(), err)

    serverMessenger := serverBackend.Messenger()

    // generate ens username
    err = serverBackend.StatusNode().EnsService().API().Add(ctx, ensChainID, ensUsername)
    require.NoError(s.T(), err)

    // generate profile showcase preferences
    profileShowcasePreferences := protocol.DummyProfileShowcasePreferences(false)
    err = serverMessenger.SetProfileShowcasePreferences(profileShowcasePreferences, false)
    require.NoError(s.T(), err)

    // generate local deleted message
    _, err = serverMessenger.CreatePublicChat(&requests.CreatePublicChat{ID: publicChatID})
    require.NoError(s.T(), err)
    serverChat := serverMessenger.Chat(publicChatID)
    serverMessage := buildTestMessage(serverChat)
    serverMessengerResponse, err := serverMessenger.SendChatMessage(ctx, serverMessage)
    require.NoError(s.T(), err)
    require.Equal(s.T(), 1, len(serverMessengerResponse.Messages()))
    serverMessageID := serverMessengerResponse.Messages()[0].ID
    _, err = serverMessenger.DeleteMessageForMeAndSync(ctx, publicChatID, serverMessageID)
    require.NoError(s.T(), err)

    err = clientBackend.AccountManager().InitKeystore(filepath.Join(clientTmpDir, keystoreDir))
    require.NoError(s.T(), err)
    err = clientBackend.OpenAccounts()
    require.NoError(s.T(), err)
    clientNodeConfig, err := nodeConfigForLocalPairSync(uuid.New().String(), "", clientTmpDir)
    require.NoError(s.T(), err)
    clientKeystoreDir := filepath.Join(clientTmpDir, keystoreDir)
    clientPayloadSourceConfig := ReceiverClientConfig{
        ReceiverConfig: &ReceiverConfig{
            KeystorePath:          clientKeystoreDir,
            DeviceType:            "iphone",
            KDFIterations:         expectedKDFIterations,
            NodeConfig:            clientNodeConfig,
            SettingCurrentNetwork: currentNetwork,
        },
        ClientConfig: new(ClientConfig),
    }
    clientNodeConfig.RootDataDir = clientTmpDir
    clientConfigBytes, err := json.Marshal(clientPayloadSourceConfig)
    require.NoError(s.T(), err)
    err = StartUpReceivingClient(clientBackend, cs, string(clientConfigBytes))
    require.NoError(s.T(), err)

    // check that the client has the same data as the server
    clientMessenger := clientBackend.Messenger()
    clientBrowserAPI := clientBackend.StatusNode().BrowserService().APIs()[0].Service.(*browsers.API)
    bookmarks, err := clientBrowserAPI.GetBookmarks(ctx)
    require.NoError(s.T(), err)
    require.Equal(s.T(), 1, len(bookmarks))
    require.Equal(s.T(), "status.im", bookmarks[0].Name)

    clientProfileShowcasePreferences, err := clientMessenger.GetProfileShowcasePreferences()
    require.NoError(s.T(), err)
    require.True(s.T(), reflect.DeepEqual(profileShowcasePreferences, clientProfileShowcasePreferences))

    uds, err := clientBackend.StatusNode().EnsService().API().GetEnsUsernames(ctx)
    require.NoError(s.T(), err)
    require.Equal(s.T(), 1, len(uds))
    require.Equal(s.T(), ensUsername, uds[0].Username)
    require.Equal(s.T(), uint64(ensChainID), uds[0].ChainID)
    deleteForMeMessages, err := clientMessenger.GetDeleteForMeMessages()
    require.NoError(s.T(), err)
    require.Equal(s.T(), 1, len(deleteForMeMessages))

    clientActiveAccount, err := clientBackend.GetActiveAccount()
    require.NoError(s.T(), err)
    require.Equal(s.T(), serverActiveAccount.Name, clientActiveAccount.Name)
    require.Equal(s.T(), clientActiveAccount.KDFIterations, expectedKDFIterations)

    require.True(s.T(), serverMessenger.HasPairedDevices())
    require.True(s.T(), clientMessenger.HasPairedDevices())

    err = serverMessenger.DisableInstallation(clientNodeConfig.ShhextConfig.InstallationID)
    require.NoError(s.T(), err)
    require.False(s.T(), serverMessenger.HasPairedDevices())
    serverNodeConfig, err := serverBackend.GetNodeConfig()
    require.NoError(s.T(), err)
    err = clientMessenger.DisableInstallation(serverNodeConfig.ShhextConfig.InstallationID)
    require.NoError(s.T(), err)
    require.False(s.T(), clientMessenger.HasPairedDevices())

    // repeat local pairing, we should expect no error after receiver logged in
    cs, err = StartUpSenderServer(serverBackend, string(configBytes))
    require.NoError(s.T(), err)
    err = StartUpReceivingClient(clientBackend, cs, string(clientConfigBytes))
    require.NoError(s.T(), err)
    require.True(s.T(), serverMessenger.HasPairedDevices())
    require.True(s.T(), clientMessenger.HasPairedDevices())

    // test if it's okay when account already exist but not logged in
    require.NoError(s.T(), clientBackend.Logout())
    cs, err = StartUpSenderServer(serverBackend, string(configBytes))
    require.NoError(s.T(), err)
    err = StartUpReceivingClient(clientBackend, cs, string(clientConfigBytes))
    require.NoError(s.T(), err)
}

func (s *SyncDeviceSuite) TestPairingThreeDevices() {
    bobTmpDir := filepath.Join(s.tmpdir, "bob")
    bobBackend := s.prepareBackendWithAccount("", bobTmpDir)
    bobMessenger := bobBackend.Messenger()
    _, err := bobMessenger.Start()
    s.Require().NoError(err)

    alice1TmpDir := filepath.Join(s.tmpdir, "alice1")
    alice1Backend := s.prepareBackendWithAccount("", alice1TmpDir)
    alice1Messenger := alice1Backend.Messenger()
    _, err = alice1Messenger.Start()
    s.Require().NoError(err)

    alice2TmpDir := filepath.Join(s.tmpdir, "alice2")
    alice2Backend := s.prepareBackendWithoutAccount(alice2TmpDir)

    alice3TmpDir := filepath.Join(s.tmpdir, "alice3")
    alice3Backend := s.prepareBackendWithoutAccount(alice3TmpDir)

    defer func() {
        require.NoError(s.T(), bobBackend.Logout())
        require.NoError(s.T(), alice1Backend.Logout())
        require.NoError(s.T(), alice2Backend.Logout())
        require.NoError(s.T(), alice3Backend.Logout())
    }()

    // Make Alice and Bob mutual contacts
    bobPublicKey := bobMessenger.GetSelfContact().ID
    request := &requests.SendContactRequest{
        ID:      bobPublicKey,
        Message: protocol.RandomLettersString(5),
    }
    s.sendContactRequest(request, alice1Messenger)
    contactRequest := s.receiveContactRequest(request.Message, bobMessenger)
    s.acceptContactRequest(contactRequest, alice1Messenger, bobMessenger)
    s.checkMutualContact(alice1Backend, bobPublicKey)

    // We shouldn't sync ourselves as a contact, so we check there's only Bob
    // https://github.com/status-im/status-go/issues/3667
    s.Require().Equal(1, len(alice1Backend.Messenger().Contacts()))

    // Pair alice-1 <-> alice-2
    s.logger.Info("pairing Alice-1 and Alice-2")
    s.pairAccounts(alice1Backend, alice1TmpDir, alice2Backend, alice2TmpDir)

    s.checkMutualContact(alice2Backend, bobPublicKey)
    s.Require().Equal(1, len(alice2Backend.Messenger().Contacts()))

    // Pair Alice-2 <-> ALice-3
    s.logger.Info("pairing Alice-2 and Alice-3")
    s.pairAccounts(alice2Backend, alice2TmpDir, alice3Backend, alice3TmpDir)

    s.checkMutualContact(alice3Backend, bobPublicKey)
    s.Require().Equal(1, len(alice3Backend.Messenger().Contacts()))
}

func (s *SyncDeviceSuite) createUser(name string) (*api.GethStatusBackend, string) {
    tmpDir := filepath.Join(s.tmpdir, name)
    backend := s.prepareBackendWithAccount("", tmpDir)
    _, err := backend.Messenger().Start()
    s.Require().NoError(err)
    return backend, tmpDir
}

func (s *SyncDeviceSuite) TestPairPendingContactRequest() {
    bobBackend, _ := s.createUser("bob")
    defer func() {
        s.Require().NoError(bobBackend.Logout())
    }()

    alice1Backend, alice1TmpDir := s.createUser("alice1")
    defer func() {
        s.Require().NoError(alice1Backend.Logout())
    }()

    // Create a pending CR from alice to bob
    bobPublicKey := bobBackend.Messenger().IdentityPublicKeyString()
    alicePublicKey := alice1Backend.Messenger().IdentityPublicKeyString()
    request := &requests.SendContactRequest{
        ID:      alicePublicKey,
        Message: protocol.RandomLettersString(5),
    }
    s.sendContactRequest(request, bobBackend.Messenger())
    contactRequest := s.receiveContactRequest(request.Message, alice1Backend.Messenger())
    s.Require().Equal(request.Message, contactRequest.Text)

    alice2TmpDir := filepath.Join(s.tmpdir, "alice2")
    alice2Backend := s.prepareBackendWithoutAccount(alice2TmpDir)
    defer func() {
        s.Require().NoError(alice2Backend.Logout())
    }()

    // Pair alice-1 <-> alice-2
    s.logger.Info("pairing Alice-1 and Alice-2")
    s.pairAccounts(alice1Backend, alice1TmpDir, alice2Backend, alice2TmpDir)

    s.logger.Debug("public keys",
        zap.String("alice", alice1Backend.Messenger().IdentityPublicKeyString()),
        zap.String("bob", bobBackend.Messenger().IdentityPublicKeyString()),
    )

    ensurePendingContact := func(m *protocol.Messenger) {
        contacts := m.Contacts()
        s.Require().Len(contacts, 1)

        c := contacts[0]
        s.Require().Equal(bobPublicKey, c.ID)
        s.Require().Equal(protocol.ContactRequestStateReceived, c.ContactRequestRemoteState)
        s.Require().Equal(protocol.ContactRequestStateNone, c.ContactRequestLocalState)

        acRequest := protocol.ActivityCenterNotificationsRequest{
            ActivityTypes: []protocol.ActivityCenterType{
                protocol.ActivityCenterNotificationTypeContactRequest,
            },
            ReadType: protocol.ActivityCenterQueryParamsReadAll,
            Limit:    10,
        }
        r, err := m.ActivityCenterNotifications(acRequest)
        s.Require().NoError(err)
        s.Require().Len(r.Notifications, 1)
    }

    // Ensure both devices have the pending Bob contact
    ensurePendingContact(alice1Backend.Messenger())
    ensurePendingContact(alice2Backend.Messenger())
}

type contactRequestAction func(messenger *protocol.Messenger, contactRequestID string) (*protocol.MessengerResponse, error)
type notificationValidateFunc func(r *protocol.ActivityCenterPaginationResponse)

func (s *SyncDeviceSuite) testPairContactRequest(requestAction contactRequestAction, validateFunc notificationValidateFunc) {
    bobBackend, _ := s.createUser("bob")
    defer func() {
        s.Require().NoError(bobBackend.Logout())
    }()

    alice1Backend, alice1TmpDir := s.createUser("alice1")
    defer func() {
        s.Require().NoError(alice1Backend.Logout())
    }()

    alicePublicKey := alice1Backend.Messenger().IdentityPublicKeyString()
    request := &requests.SendContactRequest{
        ID:      alicePublicKey,
        Message: protocol.RandomLettersString(5),
    }
    s.sendContactRequest(request, bobBackend.Messenger())
    contactRequest := s.receiveContactRequest(request.Message, alice1Backend.Messenger())
    s.Require().Equal(request.Message, contactRequest.Text)
    _, err := requestAction(alice1Backend.Messenger(), contactRequest.ID)
    s.Require().NoError(err)

    alice2TmpDir := filepath.Join(s.tmpdir, "alice2")
    alice2Backend := s.prepareBackendWithoutAccount(alice2TmpDir)
    defer func() {
        s.Require().NoError(alice2Backend.Logout())
    }()
    s.pairAccounts(alice1Backend, alice1TmpDir, alice2Backend, alice2TmpDir)

    internalNotificationValidateFunc := func(m *protocol.Messenger) {
        acRequest := protocol.ActivityCenterNotificationsRequest{
            ActivityTypes: []protocol.ActivityCenterType{
                protocol.ActivityCenterNotificationTypeContactRequest,
            },
            ReadType: protocol.ActivityCenterQueryParamsReadAll,
            Limit:    10,
        }
        r, err := m.ActivityCenterNotifications(acRequest)
        s.Require().NoError(err)
        validateFunc(r)
    }

    internalNotificationValidateFunc(alice1Backend.Messenger())
    internalNotificationValidateFunc(alice2Backend.Messenger())
}

func (s *SyncDeviceSuite) TestPairDeclineContactRequest() {
    declineContactRequest := func(messenger *protocol.Messenger, contactRequestID string) (*protocol.MessengerResponse, error) {
        return messenger.DeclineContactRequest(context.Background(), &requests.DeclineContactRequest{ID: types.Hex2Bytes(contactRequestID)})
    }
    s.testPairContactRequest(declineContactRequest, func(r *protocol.ActivityCenterPaginationResponse) {
        s.Require().Len(r.Notifications, 1)
        s.Require().False(r.Notifications[0].Accepted)
        s.Require().True(r.Notifications[0].Dismissed)
        s.Require().True(r.Notifications[0].Read)
    })
}

func (s *SyncDeviceSuite) TestPairAcceptContactRequest() {
    acceptContactRequest := func(messenger *protocol.Messenger, contactRequestID string) (*protocol.MessengerResponse, error) {
        return messenger.AcceptContactRequest(context.Background(), &requests.AcceptContactRequest{ID: types.Hex2Bytes(contactRequestID)})
    }
    s.testPairContactRequest(acceptContactRequest, func(r *protocol.ActivityCenterPaginationResponse) {
        s.Require().Len(r.Notifications, 1)
        s.Require().True(r.Notifications[0].Accepted)
        s.Require().False(r.Notifications[0].Dismissed)
        s.Require().True(r.Notifications[0].Read)
    })
}

func defaultSettings(generatedAccountInfo generator.GeneratedAccountInfo, derivedAddresses map[string]generator.AccountInfo, mnemonic *string) (*settings.Settings, error) {
    chatKeyString := derivedAddresses[pathDefaultChat].PublicKey

    syncSettings := &settings.Settings{}
    syncSettings.KeyUID = generatedAccountInfo.KeyUID
    syncSettings.Address = types.HexToAddress(generatedAccountInfo.Address)
    syncSettings.WalletRootAddress = types.HexToAddress(derivedAddresses[pathWalletRoot].Address)

    // Set chat key & name
    name, err := alias.GenerateFromPublicKeyString(chatKeyString)
    if err != nil {
        return nil, err
    }
    syncSettings.Name = name
    syncSettings.PublicKey = chatKeyString

    syncSettings.DappsAddress = types.HexToAddress(derivedAddresses[pathDefaultWallet].Address)
    syncSettings.EIP1581Address = types.HexToAddress(derivedAddresses[pathEIP1581].Address)
    syncSettings.Mnemonic = mnemonic

    syncSettings.SigningPhrase = "balabala"

    syncSettings.SendPushNotifications = true
    syncSettings.InstallationID = uuid.New().String()
    syncSettings.UseMailservers = true

    syncSettings.PreviewPrivacy = true
    syncSettings.Currency = "usd"
    syncSettings.ProfilePicturesVisibility = 1
    syncSettings.LinkPreviewRequestEnabled = true

    visibleTokens := make(map[string][]string)
    visibleTokens["mainnet"] = []string{"SNT"}
    visibleTokensJSON, err := json.Marshal(visibleTokens)
    if err != nil {
        return nil, err
    }
    visibleTokenJSONRaw := json.RawMessage(visibleTokensJSON)
    syncSettings.WalletVisibleTokens = &visibleTokenJSONRaw

    networks := `[{"id":"goerli_rpc","chain-explorer-link":"https://goerli.etherscan.io/address/","name":"Goerli with upstream RPC","config":{"NetworkId":5,"DataDir":"/ethereum/goerli_rpc","UpstreamConfig":{"Enabled":true,"URL":"https://goerli-archival.rpc.grove.city/v1/3ef2018191814b7e1009b8d9"}}},{"id":"mainnet_rpc","chain-explorer-link":"https://etherscan.io/address/","name":"Mainnet with upstream RPC","config":{"NetworkId":1,"DataDir":"/ethereum/mainnet_rpc","UpstreamConfig":{"Enabled":true,"URL":"https://eth-archival.rpc.grove.city/v1/3ef2018191814b7e1009b8d9"}}}]`
    var networksRawMessage json.RawMessage = []byte(networks)
    syncSettings.Networks = &networksRawMessage
    syncSettings.CurrentNetwork = currentNetwork

    return syncSettings, nil
}

func nodeConfigForLocalPairSync(installationID, keyUID, tmpDir string) (*params.NodeConfig, error) {
    // Set mainnet
    nodeConfig := &params.NodeConfig{}
    nodeConfig.LogEnabled = true
    nodeConfig.LogLevel = "DEBUG"
    nodeConfig.LogDir = tmpDir
    nodeConfig.KeyStoreDir = filepath.Join(keystoreDir, keyUID)
    nodeConfig.KeycardPairingDataFile = filepath.Join("keycard", "pairings.json")
    nodeConfig.ShhextConfig = params.ShhextConfig{
        InstallationID: installationID,
    }

    // need specify cluster config here, otherwise TestPairingThreeDevices will fail due to no messages(CR) received
    // TODO(frank) need to figure out why above happen
    clusterConfig, err := params.LoadClusterConfigFromFleet(params.FleetProd)
    if err != nil {
        return nil, err
    }
    nodeConfig.ClusterConfig = *clusterConfig

    return nodeConfig, nil
}

type testTimeSource struct{}

func (t *testTimeSource) GetCurrentTime() uint64 {
    return uint64(time.Now().Unix())
}

func buildTestMessage(chat *protocol.Chat) *common.Message {
    clock, timestamp := chat.NextClockAndTimestamp(&testTimeSource{})
    message := common.NewMessage()
    message.Text = "text-input-message"
    message.ChatId = chat.ID
    message.Clock = clock
    message.Timestamp = timestamp
    message.WhisperTimestamp = clock
    message.LocalChatID = chat.ID
    message.ContentType = protobuf.ChatMessage_TEXT_PLAIN
    switch chat.ChatType {
    case protocol.ChatTypePublic, protocol.ChatTypeProfile:
        message.MessageType = protobuf.MessageType_PUBLIC_GROUP
    case protocol.ChatTypeOneToOne:
        message.MessageType = protobuf.MessageType_ONE_TO_ONE
    case protocol.ChatTypePrivateGroupChat:
        message.MessageType = protobuf.MessageType_PRIVATE_GROUP
    }

    return message
}

func (s *SyncDeviceSuite) getSeedPhraseKeypairForTest(backend *api.GethStatusBackend, mnemonic string, server bool) *accounts.Keypair {
    generatedAccount, err := backend.AccountManager().AccountsGenerator().ImportMnemonic(mnemonic, "")
    require.NoError(s.T(), err)
    generatedDerivedAccs, err := backend.AccountManager().AccountsGenerator().DeriveAddresses(generatedAccount.ID, []string{path0, path1})
    require.NoError(s.T(), err)

    seedPhraseKp := &accounts.Keypair{
        KeyUID:      generatedAccount.KeyUID,
        Name:        "SeedPhraseImported",
        Type:        accounts.KeypairTypeSeed,
        DerivedFrom: generatedAccount.Address,
    }
    i := 0
    for path, ga := range generatedDerivedAccs {
        acc := &accounts.Account{
            Address:   types.HexToAddress(ga.Address),
            KeyUID:    generatedAccount.KeyUID,
            Wallet:    false,
            Chat:      false,
            Type:      accounts.AccountTypeSeed,
            Path:      path,
            PublicKey: types.HexBytes(ga.PublicKey),
            Name:      fmt.Sprintf("Acc_%d", i),
            Operable:  accounts.AccountFullyOperable,
            Emoji:     fmt.Sprintf("Emoji_%d", i),
            ColorID:   "blue",
        }
        if !server {
            acc.Operable = accounts.AccountNonOperable
        }
        seedPhraseKp.Accounts = append(seedPhraseKp.Accounts, acc)
        i++
    }

    return seedPhraseKp
}

func containsKeystoreFile(directory, key string) bool {
    files, err := os.ReadDir(directory)
    if err != nil {
        return false
    }

    for _, file := range files {
        if strings.Contains(file.Name(), strings.ToLower(key)) {
            return true
        }
    }
    return false
}

func (s *SyncDeviceSuite) TestTransferringKeystoreFiles() {
    ctx := context.TODO()

    serverTmpDir := filepath.Join(s.tmpdir, "server")
    serverBackend := s.prepareBackendWithAccount(profileKeypairMnemonic, serverTmpDir)

    clientTmpDir := filepath.Join(s.tmpdir, "client")
    clientBackend := s.prepareBackendWithAccount(profileKeypairMnemonic, clientTmpDir)
    defer func() {
        require.NoError(s.T(), clientBackend.Logout())
        require.NoError(s.T(), serverBackend.Logout())
    }()

    serverBackend.Messenger().SetLocalPairing(true)
    clientBackend.Messenger().SetLocalPairing(true)

    serverActiveAccount, err := serverBackend.GetActiveAccount()
    require.NoError(s.T(), err)

    clientActiveAccount, err := clientBackend.GetActiveAccount()
    require.NoError(s.T(), err)

    require.True(s.T(), serverActiveAccount.KeyUID == clientActiveAccount.KeyUID)

    serverSeedPhraseKp := s.getSeedPhraseKeypairForTest(serverBackend, seedKeypairMnemonic, true)
    serverAccountsAPI := serverBackend.StatusNode().AccountService().APIs()[1].Service.(*accservice.API)
    err = serverAccountsAPI.ImportMnemonic(ctx, seedKeypairMnemonic, s.password)
    require.NoError(s.T(), err, "importing mnemonic for new keypair on server")
    err = serverAccountsAPI.AddKeypair(ctx, s.password, serverSeedPhraseKp)
    require.NoError(s.T(), err, "saving seed phrase keypair on server with keystore files created")

    clientSeedPhraseKp := s.getSeedPhraseKeypairForTest(serverBackend, seedKeypairMnemonic, true)
    clientAccountsAPI := clientBackend.StatusNode().AccountService().APIs()[1].Service.(*accservice.API)
    err = clientAccountsAPI.SaveKeypair(ctx, clientSeedPhraseKp)
    require.NoError(s.T(), err, "saving seed phrase keypair on client without keystore files")

    // check server - server should contain keystore files for imported seed phrase
    serverKeystorePath := filepath.Join(serverTmpDir, keystoreDir, serverActiveAccount.KeyUID)
    require.True(s.T(), containsKeystoreFile(serverKeystorePath, serverSeedPhraseKp.DerivedFrom[2:]))
    for _, acc := range serverSeedPhraseKp.Accounts {
        require.True(s.T(), containsKeystoreFile(serverKeystorePath, acc.Address.String()[2:]))
    }

    // check client - client should not contain keystore files for imported seed phrase
    clientKeystorePath := filepath.Join(clientTmpDir, keystoreDir, clientActiveAccount.KeyUID)
    require.False(s.T(), containsKeystoreFile(clientKeystorePath, clientSeedPhraseKp.DerivedFrom[2:]))
    for _, acc := range clientSeedPhraseKp.Accounts {
        require.False(s.T(), containsKeystoreFile(clientKeystorePath, acc.Address.String()[2:]))
    }

    // prepare sender
    var config = KeystoreFilesSenderServerConfig{
        SenderConfig: &KeystoreFilesSenderConfig{
            KeystoreFilesConfig: KeystoreFilesConfig{
                KeystorePath:   serverKeystorePath,
                LoggedInKeyUID: serverActiveAccount.KeyUID,
                Password:       s.password,
            },
            KeypairsToExport: []string{serverSeedPhraseKp.KeyUID},
        },
        ServerConfig: new(ServerConfig),
    }
    configBytes, err := json.Marshal(config)
    require.NoError(s.T(), err)
    cs, err := StartUpKeystoreFilesSenderServer(serverBackend, string(configBytes))
    require.NoError(s.T(), err)

    // prepare receiver
    clientPayloadSourceConfig := KeystoreFilesReceiverClientConfig{
        ReceiverConfig: &KeystoreFilesReceiverConfig{
            KeystoreFilesConfig: KeystoreFilesConfig{
                KeystorePath:   clientKeystorePath,
                LoggedInKeyUID: clientActiveAccount.KeyUID,
                Password:       s.password,
            },
            KeypairsToImport: []string{serverSeedPhraseKp.KeyUID},
        },
        ClientConfig: new(ClientConfig),
    }
    clientConfigBytes, err := json.Marshal(clientPayloadSourceConfig)
    require.NoError(s.T(), err)
    err = StartUpKeystoreFilesReceivingClient(clientBackend, cs, string(clientConfigBytes))
    require.NoError(s.T(), err)

    // check client - client should contain keystore files for imported seed phrase
    accountManager := clientBackend.AccountManager()
    accGenerator := accountManager.AccountsGenerator()
    require.True(s.T(), containsKeystoreFile(clientKeystorePath, clientSeedPhraseKp.DerivedFrom[2:]))
    for _, acc := range clientSeedPhraseKp.Accounts {
        require.True(s.T(), containsKeystoreFile(clientKeystorePath, acc.Address.String()[2:]))
    }

    // reinit keystore on client
    require.NoError(s.T(), accountManager.InitKeystore(clientKeystorePath))

    // check keystore on client
    genAccInfo, err := accGenerator.LoadAccount(clientSeedPhraseKp.DerivedFrom, s.password)
    require.NoError(s.T(), err)
    require.Equal(s.T(), clientSeedPhraseKp.KeyUID, genAccInfo.KeyUID)
    for _, acc := range clientSeedPhraseKp.Accounts {
        genAccInfo, err := accGenerator.LoadAccount(acc.Address.String(), s.password)
        require.NoError(s.T(), err)
        require.Equal(s.T(), acc.Address.String(), genAccInfo.Address)
    }
}

func (s *SyncDeviceSuite) TestTransferringKeystoreFilesAfterStopUisngKeycard() {
    s.T().Skip("flaky test")

    ctx := context.TODO()

    // Prepare server
    serverTmpDir := filepath.Join(s.tmpdir, "server")
    serverBackend := s.prepareBackendWithAccount(profileKeypairMnemonic1, serverTmpDir)
    serverMessenger := serverBackend.Messenger()
    serverAccountsAPI := serverBackend.StatusNode().AccountService().APIs()[1].Service.(*accservice.API)

    // Prepare client
    clientTmpDir := filepath.Join(s.tmpdir, "client")
    clientBackend := s.prepareBackendWithAccount(profileKeypairMnemonic1, clientTmpDir)
    clientMessenger := clientBackend.Messenger()
    clientAccountsAPI := clientBackend.StatusNode().AccountService().APIs()[1].Service.(*accservice.API)

    defer func() {
        require.NoError(s.T(), clientBackend.Logout())
        require.NoError(s.T(), serverBackend.Logout())
    }()

    // Pair server and client
    im1 := &multidevice.InstallationMetadata{
        Name:       "client-device",
        DeviceType: "client-device-type",
    }
    settings, err := clientBackend.GetSettings()
    s.Require().NoError(err)
    err = clientMessenger.SetInstallationMetadata(settings.InstallationID, im1)
    s.Require().NoError(err)
    response, err := clientMessenger.SendPairInstallation(context.Background(), nil)
    s.Require().NoError(err)
    s.Require().NotNil(response)
    s.Require().Len(response.Chats(), 1)
    s.Require().False(response.Chats()[0].Active)

    response, err = protocol.WaitOnMessengerResponse(
        serverMessenger,
        func(r *protocol.MessengerResponse) bool {
            for _, i := range r.Installations {
                if i.ID == settings.InstallationID {
                    return true
                }
            }
            return false
        },
        "installation not received",
    )

    s.Require().NoError(err)

    found := false
    for _, i := range response.Installations {
        found = i.ID == settings.InstallationID &&
            i.InstallationMetadata != nil &&
            i.InstallationMetadata.Name == im1.Name &&
            i.InstallationMetadata.DeviceType == im1.DeviceType
        if found {
            break
        }
    }
    s.Require().True(found)

    err = serverMessenger.EnableInstallation(settings.InstallationID)
    s.Require().NoError(err)

    // Check if the logged in account is the same on server and client
    serverActiveAccount, err := serverBackend.GetActiveAccount()
    require.NoError(s.T(), err)
    clientActiveAccount, err := clientBackend.GetActiveAccount()
    require.NoError(s.T(), err)
    require.True(s.T(), serverActiveAccount.KeyUID == clientActiveAccount.KeyUID)

    //////////////////////////////////////////////////////////////////////////////
    // From this point this test is trying to simulate the following scenario:
    // - add a new seed phrase keypair on server
    // - sync it to client
    // - convert it to a keycard keypair on server
    // - sync it to client
    // - stop using keycard on server
    // - sync it to client
    // - try to transfer keystore files from server to client
    //////////////////////////////////////////////////////////////////////////////

    //////////////////////////////////////////////////////////////////////////////
    // Add new seed phrase keypair to server and sync it to client
    //////////////////////////////////////////////////////////////////////////////
    serverSeedPhraseKp := s.getSeedPhraseKeypairForTest(serverBackend, seedKeypairMnemonic1, true)
    err = serverAccountsAPI.ImportMnemonic(ctx, seedKeypairMnemonic1, s.password)
    require.NoError(s.T(), err, "importing mnemonic for new keypair on server")
    err = serverAccountsAPI.AddKeypair(ctx, s.password, serverSeedPhraseKp)
    require.NoError(s.T(), err, "saving seed phrase keypair on server with keystore files created")

    // Wait for sync messages to be received on client
    err = tt.RetryWithBackOff(func() error {
        response, err := clientMessenger.RetrieveAll()
        if err != nil {
            return err
        }

        for _, kp := range response.Keypairs {
            if kp.KeyUID == serverSeedPhraseKp.KeyUID {
                return nil
            }
        }

        return errors.New("no sync keypair received")
    })
    s.Require().NoError(err)

    // Check if the keypair saved on client is the same as the one on server
    serverKp, err := serverAccountsAPI.GetKeypairByKeyUID(ctx, serverSeedPhraseKp.KeyUID)
    s.Require().NoError(err)
    clientKp, err := clientAccountsAPI.GetKeypairByKeyUID(ctx, serverSeedPhraseKp.KeyUID)
    s.Require().NoError(err)

    s.Require().True(serverKp.KeyUID == clientKp.KeyUID &&
        serverKp.Name == clientKp.Name &&
        serverKp.Type == clientKp.Type &&
        serverKp.DerivedFrom == clientKp.DerivedFrom &&
        serverKp.LastUsedDerivationIndex == clientKp.LastUsedDerivationIndex &&
        serverKp.Clock == clientKp.Clock &&
        len(serverKp.Accounts) == len(clientKp.Accounts) &&
        len(serverKp.Keycards) == len(clientKp.Keycards))

    // Check server - server should contain keystore files for imported seed phrase
    serverKeystorePath := filepath.Join(serverTmpDir, keystoreDir, serverActiveAccount.KeyUID)
    require.True(s.T(), containsKeystoreFile(serverKeystorePath, serverKp.DerivedFrom[2:]))
    for _, acc := range serverKp.Accounts {
        require.True(s.T(), containsKeystoreFile(serverKeystorePath, acc.Address.String()[2:]))
    }

    // Check client - client should not contain keystore files for imported seed phrase
    clientKeystorePath := filepath.Join(clientTmpDir, keystoreDir, clientActiveAccount.KeyUID)
    require.False(s.T(), containsKeystoreFile(clientKeystorePath, clientKp.DerivedFrom[2:]))
    for _, acc := range clientKp.Accounts {
        require.False(s.T(), containsKeystoreFile(clientKeystorePath, acc.Address.String()[2:]))
    }

    //////////////////////////////////////////////////////////////////////////////
    // Convert it to a keycard keypair on server and sync it to client
    //////////////////////////////////////////////////////////////////////////////
    err = serverAccountsAPI.SaveOrUpdateKeycard(ctx, &accounts.Keycard{
        KeycardUID:        "1234",
        KeycardName:       "new-keycard",
        KeyUID:            serverKp.KeyUID,
        AccountsAddresses: []types.Address{serverKp.Accounts[0].Address, serverKp.Accounts[1].Address},
    }, false)
    s.Require().NoError(err)

    // Wait for sync messages to be received on client
    err = tt.RetryWithBackOff(func() error {
        response, err := clientMessenger.RetrieveAll()
        if err != nil {
            return err
        }

        for _, kp := range response.Keypairs {
            if kp.KeyUID == serverKp.KeyUID {
                return nil
            }
        }
        return errors.New("no sync keypair received")
    })
    s.Require().NoError(err)

    // Check if the keypair saved on client is the same as the one on server
    serverKp, err = serverAccountsAPI.GetKeypairByKeyUID(ctx, serverSeedPhraseKp.KeyUID)
    s.Require().NoError(err)
    clientKp, err = clientAccountsAPI.GetKeypairByKeyUID(ctx, serverSeedPhraseKp.KeyUID)
    s.Require().NoError(err)

    s.Require().True(serverKp.KeyUID == clientKp.KeyUID &&
        serverKp.Name == clientKp.Name &&
        serverKp.Type == clientKp.Type &&
        serverKp.DerivedFrom == clientKp.DerivedFrom &&
        serverKp.LastUsedDerivationIndex == clientKp.LastUsedDerivationIndex &&
        serverKp.Clock == clientKp.Clock &&
        len(serverKp.Accounts) == len(clientKp.Accounts) &&
        len(serverKp.Keycards) == len(clientKp.Keycards) &&
        len(serverKp.Keycards) == 1)

    // Check server - server should not contain keystore files for imported seed phrase
    require.False(s.T(), containsKeystoreFile(serverKeystorePath, serverKp.DerivedFrom[2:]))
    for _, acc := range serverKp.Accounts {
        require.False(s.T(), containsKeystoreFile(serverKeystorePath, acc.Address.String()[2:]))
    }

    // Check client - client should not contain keystore files for imported seed phrase
    require.False(s.T(), containsKeystoreFile(clientKeystorePath, clientKp.DerivedFrom[2:]))
    for _, acc := range clientKp.Accounts {
        require.False(s.T(), containsKeystoreFile(clientKeystorePath, acc.Address.String()[2:]))
    }

    //////////////////////////////////////////////////////////////////////////////
    // Stop using keycard on server and sync it to client
    //////////////////////////////////////////////////////////////////////////////
    err = serverAccountsAPI.MigrateNonProfileKeycardKeypairToApp(ctx, seedKeypairMnemonic1, s.password)
    s.Require().NoError(err)

    // Wait for sync messages to be received on client
    err = tt.RetryWithBackOff(func() error {
        response, err := clientMessenger.RetrieveAll()
        if err != nil {
            return err
        }

        for _, kp := range response.Keypairs {
            if kp.KeyUID == serverKp.KeyUID {
                return nil
            }
        }
        return errors.New("no sync keypair received")
    })
    s.Require().NoError(err)

    // Check if the keypair saved on client is the same as the one on server
    serverKp, err = serverAccountsAPI.GetKeypairByKeyUID(ctx, serverSeedPhraseKp.KeyUID)
    s.Require().NoError(err)
    clientKp, err = clientAccountsAPI.GetKeypairByKeyUID(ctx, serverSeedPhraseKp.KeyUID)
    s.Require().NoError(err)

    s.Require().True(serverKp.KeyUID == clientKp.KeyUID &&
        serverKp.Name == clientKp.Name &&
        serverKp.Type == clientKp.Type &&
        serverKp.DerivedFrom == clientKp.DerivedFrom &&
        serverKp.LastUsedDerivationIndex == clientKp.LastUsedDerivationIndex &&
        serverKp.Clock == clientKp.Clock &&
        len(serverKp.Accounts) == len(clientKp.Accounts) &&
        len(serverKp.Keycards) == len(clientKp.Keycards) &&
        len(serverKp.Keycards) == 0)

    // Check server - server should contain keystore files for imported seed phrase
    require.True(s.T(), containsKeystoreFile(serverKeystorePath, serverKp.DerivedFrom[2:]))
    for _, acc := range serverKp.Accounts {
        require.True(s.T(), containsKeystoreFile(serverKeystorePath, acc.Address.String()[2:]))
    }

    // Check client - client should not contain keystore files for imported seed phrase
    require.False(s.T(), containsKeystoreFile(clientKeystorePath, clientKp.DerivedFrom[2:]))
    for _, acc := range clientKp.Accounts {
        require.False(s.T(), containsKeystoreFile(clientKeystorePath, acc.Address.String()[2:]))
    }

    //////////////////////////////////////////////////////////////////////////////
    // Try to transfer keystore files from server to client
    //////////////////////////////////////////////////////////////////////////////

    serverMessenger.SetLocalPairing(true)
    clientMessenger.SetLocalPairing(true)

    // prepare sender
    var config = KeystoreFilesSenderServerConfig{
        SenderConfig: &KeystoreFilesSenderConfig{
            KeystoreFilesConfig: KeystoreFilesConfig{
                KeystorePath:   serverKeystorePath,
                LoggedInKeyUID: serverActiveAccount.KeyUID,
                Password:       s.password,
            },
            KeypairsToExport: []string{serverKp.KeyUID},
        },
        ServerConfig: new(ServerConfig),
    }
    configBytes, err := json.Marshal(config)
    require.NoError(s.T(), err)
    cs, err := StartUpKeystoreFilesSenderServer(serverBackend, string(configBytes))
    require.NoError(s.T(), err)

    // prepare receiver
    clientPayloadSourceConfig := KeystoreFilesReceiverClientConfig{
        ReceiverConfig: &KeystoreFilesReceiverConfig{
            KeystoreFilesConfig: KeystoreFilesConfig{
                KeystorePath:   clientKeystorePath,
                LoggedInKeyUID: clientActiveAccount.KeyUID,
                Password:       s.password,
            },
            KeypairsToImport: []string{clientKp.KeyUID},
        },
        ClientConfig: new(ClientConfig),
    }
    clientConfigBytes, err := json.Marshal(clientPayloadSourceConfig)
    require.NoError(s.T(), err)
    err = StartUpKeystoreFilesReceivingClient(clientBackend, cs, string(clientConfigBytes))
    require.NoError(s.T(), err)

    // Check server - server should contain keystore files for imported seed phrase
    require.True(s.T(), containsKeystoreFile(serverKeystorePath, serverKp.DerivedFrom[2:]))
    for _, acc := range serverKp.Accounts {
        require.True(s.T(), containsKeystoreFile(serverKeystorePath, acc.Address.String()[2:]))
    }

    // Check client - client should contain keystore files for imported seed phrase
    require.True(s.T(), containsKeystoreFile(clientKeystorePath, clientKp.DerivedFrom[2:]))
    for _, acc := range clientKp.Accounts {
        require.True(s.T(), containsKeystoreFile(clientKeystorePath, acc.Address.String()[2:]))
    }
}

func (s *SyncDeviceSuite) TestPreventLoggedInAccountLocalPairingClientAsReceiver() {
    clientTmpDir := filepath.Join(s.tmpdir, "client")
    clientBackend := s.prepareBackendWithAccount("", clientTmpDir)
    serverTmpDir := filepath.Join(s.tmpdir, "server")
    serverBackend := s.prepareBackendWithAccount("", serverTmpDir)
    defer func() {
        s.NoError(serverBackend.Logout())
        s.NoError(clientBackend.Logout())
    }()

    serverActiveAccount, err := serverBackend.GetActiveAccount()
    s.NoError(err)
    serverKeystorePath := filepath.Join(serverTmpDir, keystoreDir, serverActiveAccount.KeyUID)
    var config = &SenderServerConfig{
        SenderConfig: &SenderConfig{
            KeystorePath: serverKeystorePath,
            DeviceType:   "desktop",
            KeyUID:       serverActiveAccount.KeyUID,
            Password:     s.password,
        },
        ServerConfig: new(ServerConfig),
    }
    configBytes, err := json.Marshal(config)
    s.NoError(err)
    cs, err := StartUpSenderServer(serverBackend, string(configBytes))
    s.NoError(err)

    clientKeystoreDir := filepath.Join(clientTmpDir, keystoreDir)
    clientNodeConfig, err := nodeConfigForLocalPairSync(uuid.New().String(), "", clientTmpDir)
    s.NoError(err)
    clientPayloadSourceConfig := ReceiverClientConfig{
        ReceiverConfig: &ReceiverConfig{
            KeystorePath:          clientKeystoreDir,
            DeviceType:            "iphone",
            KDFIterations:         expectedKDFIterations,
            NodeConfig:            clientNodeConfig,
            SettingCurrentNetwork: currentNetwork,
        },
        ClientConfig: new(ClientConfig),
    }
    clientNodeConfig.RootDataDir = clientTmpDir
    clientConfigBytes, err := json.Marshal(clientPayloadSourceConfig)
    s.NoError(err)
    err = StartUpReceivingClient(clientBackend, cs, string(clientConfigBytes))
    s.ErrorIs(err, ErrLoggedInKeyUIDConflict)
}

func (s *SyncDeviceSuite) TestPreventLoggedInAccountLocalPairingClientAsSender() {
    clientTmpDir := filepath.Join(s.tmpdir, "client")
    clientBackend := s.prepareBackendWithAccount("", clientTmpDir)
    serverTmpDir := filepath.Join(s.tmpdir, "server")
    serverBackend := s.prepareBackendWithAccount("", serverTmpDir)
    defer func() {
        s.NoError(serverBackend.Logout())
        s.NoError(clientBackend.Logout())
    }()

    serverNodeConfig, err := nodeConfigForLocalPairSync(uuid.New().String(), "", serverTmpDir)
    s.NoError(err)
    serverKeystoreDir := filepath.Join(serverTmpDir, keystoreDir)
    serverPayloadSourceConfig := &ReceiverServerConfig{
        ReceiverConfig: &ReceiverConfig{
            NodeConfig:            serverNodeConfig,
            KeystorePath:          serverKeystoreDir,
            DeviceType:            "desktop",
            KDFIterations:         expectedKDFIterations,
            SettingCurrentNetwork: currentNetwork,
        },
        ServerConfig: new(ServerConfig),
    }
    serverNodeConfig.RootDataDir = serverTmpDir
    serverConfigBytes, err := json.Marshal(serverPayloadSourceConfig)
    s.NoError(err)
    cs, err := StartUpReceiverServer(serverBackend, string(serverConfigBytes))
    s.NoError(err)

    clientActiveAccount, err := clientBackend.GetActiveAccount()
    s.NoError(err)
    clientKeystorePath := filepath.Join(clientTmpDir, keystoreDir, clientActiveAccount.KeyUID)
    clientPayloadSourceConfig := SenderClientConfig{
        SenderConfig: &SenderConfig{
            KeystorePath: clientKeystorePath,
            DeviceType:   "android",
            KeyUID:       clientActiveAccount.KeyUID,
            Password:     s.password,
        },
        ClientConfig: new(ClientConfig),
    }
    clientConfigBytes, err := json.Marshal(clientPayloadSourceConfig)
    s.NoError(err)
    err = StartUpSendingClient(clientBackend, cs, string(clientConfigBytes))
    s.ErrorContains(err, "[client] status not ok when sending account data, received '500 Internal Server Error'")
}